From b7644319e85ce38c1a576802317a9058a6aed82d Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 7 Apr 2017 14:18:22 -0400 Subject: [PATCH] Use baked lazyloading by default The ``lazy="select"`` loader strategy now makes used of the :class:`.BakedQuery` query caching system in all cases. This removes most overhead of generating a :class:`.Query` object and running it into a :func:`.select` and then string SQL statement from the process of lazy-loading related collections and objects. The "baked" lazy loader has also been improved such that it can now cache in most cases where query load options are used. Change-Id: Ic96792fffaa045ae9aa0a4657d6d29235d3efb85 Fixes: #3954 --- doc/build/changelog/changelog_12.rst | 16 + doc/build/changelog/migration_12.rst | 36 +++ doc/build/orm/extensions/baked.rst | 40 +-- lib/sqlalchemy/ext/baked.py | 159 ++++------ lib/sqlalchemy/orm/interfaces.py | 22 ++ lib/sqlalchemy/orm/mapper.py | 4 +- lib/sqlalchemy/orm/query.py | 16 +- lib/sqlalchemy/orm/relationships.py | 25 +- lib/sqlalchemy/orm/strategies.py | 104 +++++-- lib/sqlalchemy/orm/strategy_options.py | 229 +++++++++++--- lib/sqlalchemy/orm/util.py | 13 + lib/sqlalchemy/util/_collections.py | 2 +- test/aaa_profiling/test_memusage.py | 3 +- test/ext/test_baked.py | 255 +++++++--------- test/orm/test_options.py | 399 ++++++++++++++++++++++++- test/profiles.txt | 174 +++++------ 16 files changed, 1029 insertions(+), 468 deletions(-) diff --git a/doc/build/changelog/changelog_12.rst b/doc/build/changelog/changelog_12.rst index 3b611d1476..7b47149f8a 100644 --- a/doc/build/changelog/changelog_12.rst +++ b/doc/build/changelog/changelog_12.rst @@ -42,6 +42,22 @@ that the loader options will match to an aliased or non-aliased entity more specifically if those options include entity information. + .. change:: 3954 + :tags: feature, orm + :tickets: 3954 + + The ``lazy="select"`` loader strategy now makes used of the + :class:`.BakedQuery` query caching system in all cases. This + removes most overhead of generating a :class:`.Query` object and + running it into a :func:`.select` and then string SQL statement from + the process of lazy-loading related collections and objects. The + "baked" lazy loader has also been improved such that it can now + cache in most cases where query load options are used. + + .. seealso:: + + :ref:`change_3954` + .. change:: 3740 :tags: bug, sql :tickets: 3740 diff --git a/doc/build/changelog/migration_12.rst b/doc/build/changelog/migration_12.rst index e36e0af087..c673eaa2e9 100644 --- a/doc/build/changelog/migration_12.rst +++ b/doc/build/changelog/migration_12.rst @@ -34,6 +34,42 @@ SQLAlchemy is currently tested on versions 3.5 and 3.6. New Features and Improvements - ORM =================================== +.. _change_3954: + +"Baked" loading now the default for lazy loads +---------------------------------------------- + +The :mod:`sqlalchemy.ext.baked` extension, first introduced in the 1.0 series, +allows for the construction of a so-called :class:`.BakedQuery` object, +which is an object that generates a :class:`.Query` object in conjunction +with a cache key representing the structure of the query; this cache key +is then linked to the resulting string SQL statement so that subsequent use +of another :class:`.BakedQuery` with the same structure will bypass all the +overhead of building the :class:`.Query` object, building the core +:func:`.select` object within, as well as the compilation of the :func:`.select` +into a string, cutting out well the majority of function call overhead normally +associated with constructing and emitting an ORM :class:`.Query` object. + +The :class:`.BakedQuery` is now used by default by the ORM when it generates +a "lazy" query for the lazy load of a :func:`.relationship` construct, e.g. +that of the default ``lazy="select"`` relationship loader strategy. This +will allow for a significant reduction in function calls within the scope +of an application's use of lazy load queries to load collections and related +objects. Previously, this feature was available +in 1.0 and 1.1 through the use of a global API method or by using the +``baked_select`` strategy, it's now the only implementation for this behavior. +The feature has also been improved such that the caching can still take place +for objects that have additional loader options in effect subsequent +to the lazy load. + +The caching behavior can be disabled on a per-relationship basis using the +:paramref:`.relationship.bake_queries` flag, which is available for +very unusual cases, such as a relationship that uses a custom +:class:`.Query` implementation that's not compatible with caching. + + +:ticket:`3954` + .. _change_3229: Support for bulk updates of hybrids, composites diff --git a/doc/build/orm/extensions/baked.rst b/doc/build/orm/extensions/baked.rst index 0fb8da5dfe..0dceedc4a1 100644 --- a/doc/build/orm/extensions/baked.rst +++ b/doc/build/orm/extensions/baked.rst @@ -335,37 +335,22 @@ with conditionals needed to be addressed, leading to the final approach. 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. +The baked query system is integrated into SQLAlchemy's lazy loader feature +as used by :func:`.relationship`, and will cache queries for most lazy +load conditions. A small subset of +"lazy loads" may not be cached; these involve query options in conjunction with ad-hoc +:obj:`.aliased` structures that cannot produce a repeatable cache +key. -"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. +.. versionchanged:: 1.2 "baked" queries are now the foundation of the + lazy-loader feature of :func:`.relationship`. Opting out with the bake_queries flag ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The :func:`.relationship` construct includes a flag :paramref:`.relationship.bake_queries` which when set to False will cause -that relationship to opt out of the baked query system, when the -application-wide :func:`.bake_lazy_loaders` function has been called to enable -baked query loaders by default. +that relationship to opt out of caching queries. API Documentation ----------------- @@ -378,10 +363,3 @@ API Documentation .. autoclass:: Result :members: -.. autofunction:: bake_lazy_loaders - -.. autofunction:: unbake_lazy_loaders - -.. autofunction:: baked_lazyload - -.. autofunction:: baked_lazyload_all diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py index 68bd468b97..249f5db4e9 100644 --- a/lib/sqlalchemy/ext/baked.py +++ b/lib/sqlalchemy/ext/baked.py @@ -41,10 +41,10 @@ class BakedQuery(object): self._bakery = bakery @classmethod - def bakery(cls, size=200): + def bakery(cls, size=200, _size_alert=None): """Construct a new bakery.""" - _bakery = util.LRUCache(size) + _bakery = util.LRUCache(size, size_alert=_size_alert) def call(initial_fn, *args): return cls(_bakery, initial_fn, args) @@ -134,6 +134,35 @@ class BakedQuery(object): self._spoiled = True return self + def _add_lazyload_options(self, options, effective_path): + """Used by per-state lazy loaders to add options to the + "lazy load" query from a parent query. + + Creates a cache key based on given load path and query options; + if a repeatable cache key cannot be generated, the query is + "spoiled" so that it won't use caching. + + """ + + key = () + + if effective_path.path[0].is_aliased_class: + # paths that are against an AliasedClass are unsafe to cache + # with since the AliasedClass is an ad-hoc object. + self.spoil() + else: + for opt in options: + cache_key = opt._generate_cache_key(effective_path) + if cache_key is False: + self.spoil() + elif cache_key is not None: + key += cache_key + self.add_criteria( + lambda q: q._with_current_path(effective_path). + _conditional_options(*options), + effective_path.path, key + ) + def _retrieve_baked_query(self, session): query = self._bakery.get(self._cache_key, None) if query is None: @@ -412,125 +441,31 @@ class Result(object): return None +@util.deprecated( + "1.2", "Baked lazy loading is now the default implementation.") 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. + The "baked" implementation of lazy loading is now the sole implementation + for the base lazy loader; this method has no effect except for a warning. """ - BakedLazyLoader._strategy_keys[:] = [] - - properties.RelationshipProperty.strategy_for( - lazy="select")(BakedLazyLoader) - properties.RelationshipProperty.strategy_for( - lazy=True)(BakedLazyLoader) - properties.RelationshipProperty.strategy_for( - lazy="baked_select")(BakedLazyLoader) - - strategies.LazyLoader._strategy_keys[:] = BakedLazyLoader._strategy_keys[:] + pass +@util.deprecated( + "1.2", "Baked lazy loading is now the default implementation.") 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`. + This method now raises NotImplmentedError() as the "baked" implementation + is the only lazy load implementation. The + :paramref:`.relationship.bake_queries` flag may be used to disable + the caching of queries on a per-relationship basis. """ - strategies.LazyLoader._strategy_keys[:] = [] - BakedLazyLoader._strategy_keys[:] = [] - - properties.RelationshipProperty.strategy_for( - lazy="select")(strategies.LazyLoader) - properties.RelationshipProperty.strategy_for( - lazy=True)(strategies.LazyLoader) - properties.RelationshipProperty.strategy_for( - lazy="baked_select")(BakedLazyLoader) - assert strategies.LazyLoader._strategy_keys - - -@sqla_log.class_logger -@properties.RelationshipProperty.strategy_for(lazy="baked_select") -class BakedLazyLoader(strategies.LazyLoader): - - def _emit_lazyload(self, session, state, ident_key, passive): - q = BakedQuery( - 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(full=True) - - if self.parent_property.secondary is not None: - q.add_criteria( - lambda q: - q.select_from(self.mapper, self.parent_property.secondary)) - - pending = not state.key - - # don't autoflush on pending - if pending or passive & attributes.NO_AUTOFLUSH: - q.add_criteria(lambda q: q.autoflush(False)) - - if state.load_options: - q.spoil() - args = state.load_path[self.parent_property] - q.add_criteria( - lambda q: - q._with_current_path(args), args) - q.add_criteria( - lambda q: q._conditional_options(*state.load_options)) - - if self.use_get: - return q(session)._load_on_ident( - session.query(self.mapper), ident_key) - - if self.parent_property.order_by: - q.add_criteria( - lambda q: - q.order_by(*util.to_list(self.parent_property.order_by))) - - for rev in self.parent_property._reverse_property: - # reverse props that are MANYTOONE are loading *this* - # object from get(), so don't need to eager out to those. - if rev.direction is interfaces.MANYTOONE and \ - rev._use_get and \ - not isinstance(rev.strategy, strategies.LazyLoader): - - q.add_criteria( - lambda q: - q.options( - strategy_options.Load.for_existing_path( - q._current_path[rev.parent] - ).baked_lazyload(rev.key) - ) - ) - - lazy_clause, params = self._generate_lazy_clause(state, passive) - - if pending: - if orm_util._none_set.intersection(params.values()): - return None - - q.add_criteria(lambda q: q.filter(lazy_clause)) - result = q(session).params(**params).all() - if self.uselist: - return result - else: - l = len(result) - if l: - if l > 1: - util.warn( - "Multiple rows returned with " - "uselist=False for lazily-loaded attribute '%s' " - % self.parent_property) - - return result[0] - else: - return None + raise NotImplementedError( + "Baked lazy loading is now the default implementation") @strategy_options.loader_option() @@ -543,12 +478,18 @@ def baked_lazyload(loadopt, attr): @baked_lazyload._add_unbound_fn +@util.deprecated( + "1.2", "Baked lazy loading is now the default " + "implementation for lazy loading.") def baked_lazyload(*keys): return strategy_options._UnboundLoad._from_keys( strategy_options._UnboundLoad.baked_lazyload, keys, False, {}) @baked_lazyload._add_unbound_all_fn +@util.deprecated( + "1.2", "Baked lazy loading is now the default " + "implementation for lazy loading.") def baked_lazyload_all(*keys): return strategy_options._UnboundLoad._from_keys( strategy_options._UnboundLoad.baked_lazyload, keys, True, {}) diff --git a/lib/sqlalchemy/orm/interfaces.py b/lib/sqlalchemy/orm/interfaces.py index 1b14acefb9..84b5f6cc7a 100644 --- a/lib/sqlalchemy/orm/interfaces.py +++ b/lib/sqlalchemy/orm/interfaces.py @@ -593,6 +593,28 @@ class MapperOption(object): self.process_query(query) + def _generate_cache_key(self, path): + """Used by the baked loader to see if this option can be cached. + + A given MapperOption that returns a cache key must return a key + that uniquely identifies the complete state of this option, which + will match any other MapperOption that itself retains the identical + state. This includes path options, flags, etc. + + If the MapperOption does not apply to the given path and would + not affect query results on such a path, it should return None. + + if the MapperOption **does** apply to the give path, however cannot + produce a safe cache key, it should return False; this will cancel + caching of the result. An unsafe cache key is one that includes + an ad-hoc user object, typically an AliasedClass object. As these + are usually created per-query, they don't work as cache keys. + + + """ + + return None + class LoaderStrategy(object): """Describe the loading behavior of a StrategizedProperty object. diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index 28a5c5b9bf..3fdba44825 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -2721,7 +2721,7 @@ class Mapper(InspectionAttr): return util.LRUCache(self._compiled_cache_size, size_alert=self._alert_lru_cache_limit) - def _alert_lru_cache_limit(self): + def _alert_lru_cache_limit(self, lru_cache): util.warn( "Compiled statement cache for mapper %s is " "reaching its size threshold of %d, based on _compiled_cache_size " @@ -2730,7 +2730,7 @@ class Mapper(InspectionAttr): "#faq_compiled_cache_threshold" " for best practices." % (self, - self._compiled_cache.size_threshold, + lru_cache.size_threshold, self._compiled_cache_size)) @_memoized_configured_property diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index bbfa393f8c..272ef77fb8 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -29,7 +29,8 @@ from .base import _entity_descriptor, _is_aliased_class, \ _is_mapped_class, _orm_columns, _generative, InspectionAttr from .path_registry import PathRegistry from .util import ( - AliasedClass, ORMAdapter, join as orm_join, with_parent, aliased + AliasedClass, ORMAdapter, join as orm_join, with_parent, aliased, + _entity_corresponds_to ) from .. import sql, util, log, exc as sa_exc, inspect, inspection from ..sql.expression import _interpret_as_from @@ -3641,18 +3642,7 @@ class _MapperEntity(_QueryEntity): return self.entity_zero def corresponds_to(self, entity): - if entity.is_aliased_class: - if self.is_aliased_class: - if entity._base_alias is self.entity_zero._base_alias: - return True - return False - elif self.is_aliased_class: - if self.entity_zero._use_mapper_path: - return entity in self._with_polymorphic - else: - return entity is self.entity_zero - - return entity.common_parent(self.entity_zero) + return _entity_corresponds_to(self.entity_zero, entity) def adapt_to_selectable(self, query, sel): query._entities.append(self) diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index 3b83f10cdc..1005e7eeb4 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -102,7 +102,7 @@ class RelationshipProperty(StrategizedProperty): back_populates=None, post_update=False, cascade=False, extension=None, - viewonly=False, lazy=True, + viewonly=False, lazy="select", collection_class=None, passive_deletes=False, passive_updates=True, remote_side=None, enable_typechecks=True, join_depth=None, @@ -277,22 +277,13 @@ class RelationshipProperty(StrategizedProperty): :param bake_queries=True: Use the :class:`.BakedQuery` cache to cache the construction of SQL - used in lazy loads, when the :func:`.bake_lazy_loaders` function has - first been called. Defaults to True and is intended to provide an - "opt out" flag per-relationship when the baked query cache system is - in use. - - .. warning:: - - This flag **only** has an effect when the application-wide - :func:`.bake_lazy_loaders` function has been called. It - defaults to True so is an "opt out" flag. - - Setting this flag to False when baked queries are otherwise in - use might be to reduce - ORM memory use for this :func:`.relationship`, or to work around - unresolved stability issues observed within the baked query - cache system. + used in lazy loads. True by default. Set to False if the + join condition of the relationship has unusual features that + might not respond well to statement caching. + + .. versionchanged:: 1.2 + "Baked" loading is the default implementation for the "select", + a.k.a. "lazy" loading strategy for relationships. .. versionadded:: 1.0.0 diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index c70994e8f0..fdcb54953a 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -371,6 +371,7 @@ class NoLoader(AbstractRelationshipLoader): @properties.RelationshipProperty.strategy_for(lazy="select") @properties.RelationshipProperty.strategy_for(lazy="raise") @properties.RelationshipProperty.strategy_for(lazy="raise_on_sql") +@properties.RelationshipProperty.strategy_for(lazy="baked_select") class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): """Provide loading behavior for a :class:`.RelationshipProperty` with "lazy=True", that is loads when first accessed. @@ -380,7 +381,8 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): __slots__ = ( '_lazywhere', '_rev_lazywhere', 'use_get', '_bind_to_col', '_equated_columns', '_rev_bind_to_col', '_rev_equated_columns', - '_simple_lazy_clause', '_raise_always', '_raise_on_sql') + '_simple_lazy_clause', '_raise_always', '_raise_on_sql', + '_bakery') def __init__(self, parent, strategy_key): super(LazyLoader, self).__init__(parent, strategy_key) @@ -575,35 +577,90 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): for pk in self.mapper.primary_key ] - @util.dependencies("sqlalchemy.orm.strategy_options") + @util.dependencies("sqlalchemy.ext.baked") + def _memoized_attr__bakery(self, baked): + return baked.bakery(size=50, _size_alert=self._alert_lru_cache_limit) + + def _alert_lru_cache_limit(self, lru_cache): + util.warn( + "Compiled statement cache for lazy loader on attribute %s is " + "reaching its size threshold of %d. Consider setting " + "bake_queries=False for this relationship. Please refer to " + "http://docs.sqlalchemy.org/en/latest/faq/performance.html" + "#faq_compiled_cache_threshold" + " for best practices." % + (self.parent_property, + lru_cache.size_threshold)) + + @util.dependencies( + "sqlalchemy.orm.strategy_options") def _emit_lazyload( self, strategy_options, session, state, ident_key, passive): - q = session.query(self.mapper)._adapt_all_clauses() - if self.parent_property.secondary is not None: - q = q.select_from(self.mapper, self.parent_property.secondary) + # emit lazy load now using BakedQuery, to cut way down on the overhead + # of generating queries. + # there are two big things we are trying to guard against here: + # + # 1. two different lazy loads that need to have a different result, + # being cached on the same key. The results between two lazy loads + # can be different due to the options passed to the query, which + # take effect for descendant objects. Therefore we have to make + # sure paths and load options generate good cache keys, and if they + # don't, we don't cache. + # 2. a lazy load that gets cached on a key that includes some + # "throwaway" object, like a per-query AliasedClass, meaning + # the cache key will never be seen again and the cache itself + # will fill up. (the cache is an LRU cache, so while we won't + # run out of memory, it will perform terribly when it's full. A + # warning is emitted if this occurs.) We must prevent the + # generation of a cache key that is including a throwaway object + # in the key. + + # note that "lazy='select'" and "lazy=True" make two separate + # lazy loaders. Currently the LRU cache is local to the LazyLoader, + # however add ourselves to the initial cache key just to future + # proof in case it moves + q = self._bakery(lambda session: session.query(self.mapper), self) + + 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(full=True) - q = q._with_invoke_all_eagers(False) + if self.parent_property.secondary is not None: + q.add_criteria( + lambda q: + q.select_from(self.mapper, self.parent_property.secondary)) pending = not state.key # don't autoflush on pending if pending or passive & attributes.NO_AUTOFLUSH: - q = q.autoflush(False) - - if state.load_path: - q = q._with_current_path(state.load_path[self.parent_property]) + q.add_criteria(lambda q: q.autoflush(False)) if state.load_options: - q = q._conditional_options(*state.load_options) + # here, if any of the options cannot return a cache key, + # the BakedQuery "spoils" and caching will not occur. a path + # that features Cls.attribute.of_type(some_alias) will cancel + # caching, for example, since "some_alias" is user-defined and + # is usually a throwaway object. + effective_path = state.load_path[self.parent_property] + q._add_lazyload_options( + state.load_options, effective_path + ) if self.use_get: if self._raise_on_sql: self._invoke_raise_load(state, passive, "raise_on_sql") - return loading.load_on_ident(q, ident_key) + return q(session)._load_on_ident( + session.query(self.mapper), ident_key) if self.parent_property.order_by: - q = q.order_by(*util.to_list(self.parent_property.order_by)) + q.add_criteria( + lambda q: + q.order_by(*util.to_list(self.parent_property.order_by))) for rev in self.parent_property._reverse_property: # reverse props that are MANYTOONE are loading *this* @@ -611,28 +668,31 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): if rev.direction is interfaces.MANYTOONE and \ rev._use_get and \ not isinstance(rev.strategy, LazyLoader): - q = q.options( - strategy_options.Load.for_existing_path( - q._current_path[rev.parent] - ).lazyload(rev.key) + + q.add_criteria( + lambda q: + q.options( + strategy_options.Load.for_existing_path( + q._current_path[rev.parent] + ).lazyload(rev.key) + ) ) - lazy_clause, params = self._generate_lazy_clause( - state, passive=passive) + lazy_clause, params = self._generate_lazy_clause(state, passive) if pending: if util.has_intersection( orm_util._none_set, params.values()): return None + elif util.has_intersection(orm_util._never_set, params.values()): return None if self._raise_on_sql: self._invoke_raise_load(state, passive, "raise_on_sql") - q = q.filter(lazy_clause).params(params) - - result = q.all() + q.add_criteria(lambda q: q.filter(lazy_clause)) + result = q(session).params(**params).all() if self.uselist: return result else: diff --git a/lib/sqlalchemy/orm/strategy_options.py b/lib/sqlalchemy/orm/strategy_options.py index 60ed2be304..e67159b0dc 100644 --- a/lib/sqlalchemy/orm/strategy_options.py +++ b/lib/sqlalchemy/orm/strategy_options.py @@ -8,7 +8,8 @@ """ -from .interfaces import MapperOption, PropComparator +from .interfaces import MapperOption, PropComparator, MapperProperty +from .attributes import QueryableAttribute from .. import util from ..sql.base import _generative, Generative from .. import exc as sa_exc, inspect @@ -82,8 +83,9 @@ class Load(Generative, MapperOption): self.path = insp._path_registry # note that this .context is shared among all descendant # Load objects - self.context = {} + self.context = util.OrderedDict() self.local_opts = {} + self._of_type = None @classmethod def for_existing_path(cls, path): @@ -91,8 +93,57 @@ class Load(Generative, MapperOption): load.path = path load.context = {} load.local_opts = {} + load._of_type = None return load + def _generate_cache_key(self, path): + if path.path[0].is_aliased_class: + return False + + serialized = [] + for (key, loader_path), obj in self.context.items(): + if key != "loader": + continue + + endpoint = obj._of_type or obj.path.path[-1] + chopped = self._chop_path(loader_path, path) + if not chopped and not obj._of_type: + continue + + serialized_path = [] + + for token in chopped: + if isinstance(token, util.string_types): + serialized_path.append(token) + elif token.is_aliased_class: + return False + elif token.is_property: + serialized_path.append(token.key) + else: + assert token.is_mapper + serialized_path.append(token.class_) + + if not serialized_path or endpoint != serialized_path[-1]: + if endpoint.is_mapper: + serialized_path.append(endpoint.class_) + elif endpoint.is_aliased_class: + return False + + serialized.append( + ( + tuple(serialized_path) + + obj.strategy + + (tuple([ + (key, obj.local_opts[key]) + for key in sorted(obj.local_opts) + ]) if obj.local_opts else ()) + ) + ) + if not serialized: + return None + else: + return tuple(serialized) + def _generate(self): cloned = super(Load, self)._generate() cloned.local_opts = {} @@ -119,6 +170,7 @@ class Load(Generative, MapperOption): query._attributes.update(self.context) def _generate_path(self, path, attr, wildcard_key, raiseerr=True): + self._of_type = None if raiseerr and not path.has_entity: if isinstance(path, TokenRegistry): raise sa_exc.ArgumentError( @@ -137,7 +189,9 @@ class Load(Generative, MapperOption): self.propagate_to_loaders = False if wildcard_key: attr = "%s:%s" % (wildcard_key, attr) - return path.token(attr) + path = path.token(attr) + self.path = path + return path try: # use getattr on the class to work around @@ -171,7 +225,6 @@ class Load(Generative, MapperOption): ac = attr._of_type ext_info = inspect(ac) - path_element = ext_info.mapper existing = path.entity_path[prop].get( self.context, "path_with_polymorphic") if not ext_info.is_aliased_class: @@ -182,12 +235,25 @@ class Load(Generative, MapperOption): _existing_alias=existing) path.entity_path[prop].set( self.context, "path_with_polymorphic", inspect(ac)) - path = path[prop][path_element] + + # the path here will go into the context dictionary and + # needs to match up to how the class graph is traversed. + # so we can't put an AliasedInsp in the path here, needs + # to be the base mapper. + path = path[prop][ext_info.mapper] + + # but, we need to know what the original of_type() + # argument is for cache key purposes. so....store that too. + # it might be better for "path" to really represent, + # "the path", but trying to keep the impact of the cache + # key feature localized for now + self._of_type = ext_info else: path = path[prop] if path.has_entity: path = path.entity_path + self.path = path return path def __str__(self): @@ -205,7 +271,7 @@ class Load(Generative, MapperOption): self.propagate_to_loaders = propagate_to_loaders # if the path is a wildcard, this will set propagate_to_loaders=False - self.path = self._generate_path(self.path, attr, "relationship") + self._generate_path(self.path, attr, "relationship") self.strategy = strategy if strategy is not None: self._set_path_strategy() @@ -215,10 +281,9 @@ class Load(Generative, MapperOption): strategy = self._coerce_strat(strategy) for attr in attrs: - path = self._generate_path(self.path, attr, "column") cloned = self._generate() cloned.strategy = strategy - cloned.path = path + cloned._generate_path(self.path, attr, "column") cloned.propagate_to_loaders = True if opts: cloned.local_opts.update(opts) @@ -285,7 +350,7 @@ class _UnboundLoad(Load): """Represent a loader option that isn't tied to a root entity. The loader option will produce an entity-linked :class:`.Load` - object when it is passed :meth:`.Query.options`. + object when it is passed :metfh:`.Query.options`. This provides compatibility with the traditional system of freestanding options, e.g. ``joinedload('x.y.z')``. @@ -294,13 +359,30 @@ class _UnboundLoad(Load): def __init__(self): self.path = () - self._to_bind = set() + self._to_bind = [] self.local_opts = {} _is_chain_link = False + def _generate_cache_key(self, path): + serialized = () + for val in self._to_bind: + opt = val._bind_loader( + [path.path[0]], + None, None, False) + if opt: + c_key = opt._generate_cache_key(path) + if c_key is False: + return False + elif c_key: + serialized += c_key + if not serialized: + return None + else: + return serialized + def _set_path_strategy(self): - self._to_bind.add(self) + self._to_bind.append(self) def _generate_path(self, path, attr, wildcard_key): if wildcard_key and isinstance(attr, util.string_types) and \ @@ -308,25 +390,28 @@ class _UnboundLoad(Load): if attr == _DEFAULT_TOKEN: self.propagate_to_loaders = False attr = "%s:%s" % (wildcard_key, attr) - - return path + (attr, ) + path = path + (attr, ) + self.path = path + return path def __getstate__(self): d = self.__dict__.copy() - d['path'] = ret = [] - for token in util.to_list(self.path): - if isinstance(token, PropComparator): - ret.append((token._parentmapper.class_, token.key)) - else: - ret.append(token) + d['path'] = self._serialize_path(self.path) return d def __setstate__(self, state): ret = [] for key in state['path']: if isinstance(key, tuple): - cls, propkey = key - ret.append(getattr(cls, propkey)) + if len(key) == 2: + # support legacy + cls, propkey = key + else: + cls, propkey, of_type = key + prop = getattr(cls, propkey) + if of_type: + prop = prop.of_type(prop) + ret.append(prop) else: ret.append(key) state['path'] = tuple(ret) @@ -334,7 +419,9 @@ class _UnboundLoad(Load): def _process(self, query, raiseerr): for val in self._to_bind: - val._bind_loader(query, query._attributes, raiseerr) + val._bind_loader( + [ent.entity_zero for ent in query._mapper_entities], + query._current_path, query._attributes, raiseerr) @classmethod def _from_keys(cls, meth, keys, chained, kw): @@ -384,26 +471,80 @@ class _UnboundLoad(Load): return to_chop[i:] - def _bind_loader(self, query, context, raiseerr): + def _serialize_path(self, path, reject_aliased_class=False): + ret = [] + for token in path: + if isinstance(token, QueryableAttribute): + if reject_aliased_class and ( + (token._of_type and + inspect(token._of_type).is_aliased_class) + or + inspect(token.parent).is_aliased_class + ): + return False + ret.append( + (token._parentmapper.class_, token.key, token._of_type)) + elif isinstance(token, PropComparator): + ret.append((token._parentmapper.class_, token.key, None)) + else: + ret.append(token) + return ret + + def _bind_loader(self, entities, current_path, context, raiseerr): + """Convert from an _UnboundLoad() object into a Load() object. + + The _UnboundLoad() uses an informal "path" and does not necessarily + refer to a lead entity as it may use string tokens. The Load() + OTOH refers to a complete path. This method reconciles from a + given Query into a Load. + + Example:: + + + query = session.query(User).options( + joinedload("orders").joinedload("items")) + + The above options will be an _UnboundLoad object along the lines + of (note this is not the exact API of _UnboundLoad):: + + _UnboundLoad( + _to_bind=[ + _UnboundLoad(["orders"], {"lazy": "joined"}), + _UnboundLoad(["orders", "items"], {"lazy": "joined"}), + ] + ) + + After this method, we get something more like this (again this is + not exact API):: + + Load( + User, + (User, User.orders.property)) + Load( + User, + (User, User.orders.property, Order, Order.items.property)) + + """ start_path = self.path # _current_path implies we're in a # secondary load with an existing path - current_path = query._current_path if current_path: start_path = self._chop_path(start_path, current_path) if not start_path: return None + # look at the first token and try to locate within the Query + # what entity we are referring towards. token = start_path[0] if isinstance(token, util.string_types): - entity = self._find_entity_basestring(query, token, raiseerr) + entity = self._find_entity_basestring(entities, token, raiseerr) elif isinstance(token, PropComparator): prop = token.property entity = self._find_entity_prop_comparator( - query, + entities, prop.key, token._parententity, raiseerr) @@ -416,20 +557,26 @@ class _UnboundLoad(Load): if not entity: return - path_element = entity.entity_zero + path_element = entity # transfer our entity-less state into a Load() object - # with a real entity path. + # with a real entity path. Start with the lead entity + # we just located, then go through the rest of our path + # tokens and populate into the Load(). loader = Load(path_element) - loader.context = context + + if context is not None: + loader.context = context + else: + context = loader.context + loader.strategy = self.strategy loader.is_opts_only = self.is_opts_only path = loader.path for token in start_path: - loader.path = path = loader._generate_path( - loader.path, token, None, raiseerr) - if path is None: + if not loader._generate_path( + loader.path, token, None, raiseerr): return loader.local_opts.update(self.local_opts) @@ -455,17 +602,19 @@ class _UnboundLoad(Load): replace=not self._is_chain_link, merge_opts=self.is_opts_only) - def _find_entity_prop_comparator(self, query, token, mapper, raiseerr): + return loader + + def _find_entity_prop_comparator(self, entities, token, mapper, raiseerr): if _is_aliased_class(mapper): searchfor = mapper else: searchfor = _class_to_mapper(mapper) - for ent in query._mapper_entities: - if ent.corresponds_to(searchfor): + for ent in entities: + if orm_util._entity_corresponds_to(ent, searchfor): return ent else: if raiseerr: - if not list(query._mapper_entities): + if not list(entities): raise sa_exc.ArgumentError( "Query has only expression-based entities - " "can't find property named '%s'." @@ -477,14 +626,14 @@ class _UnboundLoad(Load): "specified in this Query. Note the full path " "from root (%s) to target entity must be specified." % (token, ",".join(str(x) for - x in query._mapper_entities)) + x in entities)) ) else: return None - def _find_entity_basestring(self, query, token, raiseerr): + def _find_entity_basestring(self, entities, token, raiseerr): if token.endswith(':' + _WILDCARD_TOKEN): - if len(list(query._mapper_entities)) != 1: + if len(list(entities)) != 1: if raiseerr: raise sa_exc.ArgumentError( "Wildcard loader can only be used with exactly " @@ -493,7 +642,7 @@ class _UnboundLoad(Load): elif token.endswith(_DEFAULT_TOKEN): raiseerr = False - for ent in query._mapper_entities: + for ent in entities: # return only the first _MapperEntity when searching # based on string prop name. Ideally object # attributes are used to specify more exactly. diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index 73b0be99ce..2e61661eda 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -1039,6 +1039,19 @@ def was_deleted(object): state = attributes.instance_state(object) return state.was_deleted +def _entity_corresponds_to(given, entity): + if entity.is_aliased_class: + if given.is_aliased_class: + if entity._base_alias is given._base_alias: + return True + return False + elif given.is_aliased_class: + if given._use_mapper_path: + return entity in given.with_polymorphic_mappers + else: + return entity is given + + return entity.common_parent(given) def randomize_unitofwork(): """Use random-ordering sets within the unit of work in order diff --git a/lib/sqlalchemy/util/_collections.py b/lib/sqlalchemy/util/_collections.py index d94af5f62a..32d9c6190a 100644 --- a/lib/sqlalchemy/util/_collections.py +++ b/lib/sqlalchemy/util/_collections.py @@ -925,7 +925,7 @@ class LRUCache(dict): while len(self) > self.capacity + self.capacity * self.threshold: if size_alert: size_alert = False - self.size_alert() + self.size_alert(self) by_counter = sorted(dict.values(self), key=operator.itemgetter(2), reverse=True) diff --git a/test/aaa_profiling/test_memusage.py b/test/aaa_profiling/test_memusage.py index e038d5c387..4a378d77dd 100644 --- a/test/aaa_profiling/test_memusage.py +++ b/test/aaa_profiling/test_memusage.py @@ -263,7 +263,8 @@ class MemUsageWBackendTest(EnsureZeroed): del sessmaker go() - @testing.emits_warning("Compiled statement cache for.*") + @testing.emits_warning("Compiled statement cache for mapper.*") + @testing.emits_warning("Compiled statement cache for lazy loader.*") @testing.crashes('sqlite', ':memory: connection not suitable here') def test_orm_many_engines(self): metadata = MetaData(self.engine) diff --git a/test/ext/test_baked.py b/test/ext/test_baked.py index a5eb314950..263a1bb6c3 100644 --- a/test/ext/test_baked.py +++ b/test/ext/test_baked.py @@ -1,10 +1,12 @@ from sqlalchemy.orm import Session, subqueryload, \ - mapper, relationship, lazyload, clear_mappers, backref + mapper, relationship, lazyload, clear_mappers, backref, aliased, \ + Load, defaultload from sqlalchemy.testing import eq_, is_, is_not_ from sqlalchemy.testing import assert_raises, assert_raises_message from sqlalchemy import testing +from sqlalchemy import inspect from test.orm import _fixtures -from sqlalchemy.ext.baked import BakedQuery, baked_lazyload, BakedLazyLoader +from sqlalchemy.ext.baked import BakedQuery from sqlalchemy.ext import baked from sqlalchemy import bindparam, func, literal_column from sqlalchemy.orm import exc as orm_exc @@ -719,6 +721,22 @@ class LazyLoaderTest(testing.AssertsCompiledSQL, BakedTest): mapper(Address, self.tables.addresses) return User, Address + def _o2m_twolevel_fixture(self, lazy="select", **kw): + User = self.classes.User + Address = self.classes.Address + Dingaling = self.classes.Dingaling + + mapper(User, self.tables.users, properties={ + 'addresses': relationship( + Address, order_by=self.tables.addresses.c.id, + lazy=lazy, **kw) + }) + mapper(Address, self.tables.addresses, properties={ + "dingalings": relationship(Dingaling, lazy=lazy) + }) + mapper(Dingaling, self.tables.dingalings) + return User, Address, Dingaling + def _m2o_fixture(self): User = self.classes.User Address = self.classes.Address @@ -729,124 +747,81 @@ class LazyLoaderTest(testing.AssertsCompiledSQL, BakedTest): }) return User, Address - def test_strategy_lookup(self): - """test that the lazy loader strategies aren't getting mixed up - with BakedLazyLoader as a subclass. - - """ - User, Address = self._o2m_fixture() - - ll = User.addresses.property._get_strategy((('lazy', 'select'),)) - assert not isinstance(ll, BakedLazyLoader) - eq_(ll._strategy_keys, [(('lazy', 'select'),), (('lazy', True),)]) - - ll = User.addresses.property._get_strategy((('lazy', True),)) - assert not isinstance(ll, BakedLazyLoader) - eq_(ll._strategy_keys, [(('lazy', 'select'),), (('lazy', True),)]) - - bl = User.addresses.property._get_strategy((('lazy', 'baked_select'),)) - assert isinstance(bl, BakedLazyLoader) - eq_(bl._strategy_keys, [(('lazy', 'baked_select'),)]) - - def test_invocation_per_state(self): - """test that BakedLazyLoader is getting invoked with the - baked_lazyload() loader. - - """ - User, Address = self._o2m_fixture() - - sess = Session() - q = sess.query(User) - - with mock.patch.object(BakedLazyLoader, "_emit_lazyload") as el: - u1 = q.first() - u1.addresses - # not invoked - eq_(el.mock_calls, []) - - sess = Session() - q = sess.query(User).options(baked_lazyload(User.addresses)) - with mock.patch.object(BakedLazyLoader, "_emit_lazyload") as el: - u1 = q.first() - u1.addresses - # invoked - is_( - el.mock_calls[0][1][1], - u1._sa_instance_state - ) - - def test_invocation_per_mapper(self): - """test that BakedLazyLoader is getting invoked with the - "baked_select" lazy setting. - - """ - User, Address = self._o2m_fixture(lazy="baked_select") + def test_unsafe_unbound_option_cancels_bake(self): + User, Address, Dingaling = self._o2m_twolevel_fixture(lazy="joined") - sess = Session() - q = sess.query(User).options(lazyload(User.addresses)) - - with mock.patch.object(BakedLazyLoader, "_emit_lazyload") as el: - u1 = q.first() - u1.addresses - # not invoked - eq_(el.mock_calls, []) - - sess = Session() - q = sess.query(User) - with mock.patch.object(BakedLazyLoader, "_emit_lazyload") as el: - u1 = q.first() - u1.addresses - # invoked - is_( - el.mock_calls[0][1][1], - u1._sa_instance_state - ) - - def test_systemwide_loaders_loadable_via_lazyloader(self): - from sqlalchemy.orm import configure_mappers - - baked.bake_lazy_loaders() - try: - User, Address = self._o2m_fixture(lazy='joined') - - configure_mappers() - - is_( - User.addresses.property. - _get_strategy((('lazy', 'select'), )).__class__, - BakedLazyLoader - ) - finally: - baked.unbake_lazy_loaders() - - def test_invocation_systemwide_loaders(self): - baked.bake_lazy_loaders() - try: - User, Address = self._o2m_fixture() + class SubDingaling(Dingaling): + pass + mapper(SubDingaling, None, inherits=Dingaling) + lru = Address.dingalings.property._lazy_strategy._bakery( + lambda q: None)._bakery + l1 = len(lru) + for i in range(5): sess = Session() - q = sess.query(User).options(lazyload(User.addresses)) - with mock.patch.object(BakedLazyLoader, "_emit_lazyload") as el: - u1 = q.first() - u1.addresses - # invoked - is_( - el.mock_calls[0][1][1], - u1._sa_instance_state - ) - finally: - baked.unbake_lazy_loaders() - - clear_mappers() - User, Address = self._o2m_fixture() - sess = Session() - q = sess.query(User).options(lazyload(User.addresses)) + u1 = sess.query(User).options( + defaultload(User.addresses).lazyload( + Address.dingalings.of_type(aliased(SubDingaling)))).first() + for ad in u1.addresses: + ad.dingalings + l2 = len(lru) + eq_(l1, 0) + eq_(l2, 1) + + def test_unsafe_bound_option_cancels_bake(self): + User, Address, Dingaling = self._o2m_twolevel_fixture(lazy="joined") + + class SubDingaling(Dingaling): + pass + mapper(SubDingaling, None, inherits=Dingaling) - with mock.patch.object(BakedLazyLoader, "_emit_lazyload") as el: - u1 = q.first() - u1.addresses - # not invoked - eq_(el.mock_calls, []) + lru = Address.dingalings.property._lazy_strategy._bakery( + lambda q: None)._bakery + l1 = len(lru) + for i in range(5): + sess = Session() + u1 = sess.query(User).options( + Load(User).defaultload(User.addresses).lazyload( + Address.dingalings.of_type(aliased(SubDingaling)))).first() + for ad in u1.addresses: + ad.dingalings + l2 = len(lru) + eq_(l1, 0) + eq_(l2, 1) + + def test_safe_unbound_option_allows_bake(self): + User, Address, Dingaling = self._o2m_twolevel_fixture(lazy="joined") + + lru = Address.dingalings.property._lazy_strategy._bakery( + lambda q: None)._bakery + l1 = len(lru) + for i in range(5): + sess = Session() + u1 = sess.query(User).options( + defaultload(User.addresses).lazyload( + Address.dingalings)).first() + for ad in u1.addresses: + ad.dingalings + l2 = len(lru) + eq_(l1, 0) + eq_(l2, 2) + + def test_safe_bound_option_allows_bake(self): + User, Address, Dingaling = self._o2m_twolevel_fixture(lazy="joined") + + lru = Address.dingalings.property._lazy_strategy._bakery( + lambda q: None)._bakery + l1 = len(lru) + for i in range(5): + sess = Session() + u1 = sess.query(User).options( + Load(User).defaultload(User.addresses).lazyload( + Address.dingalings)).first() + for ad in u1.addresses: + ad.dingalings + l2 = len(lru) + eq_(l1, 0) + eq_(l2, 2) def test_baked_lazy_loading_relationship_flag_true(self): self._test_baked_lazy_loading_relationship_flag(True) @@ -855,37 +830,33 @@ class LazyLoaderTest(testing.AssertsCompiledSQL, BakedTest): self._test_baked_lazy_loading_relationship_flag(False) def _test_baked_lazy_loading_relationship_flag(self, flag): - baked.bake_lazy_loaders() - try: - User, Address = self._o2m_fixture(bake_queries=flag) + User, Address = self._o2m_fixture(bake_queries=flag) - sess = Session() - u1 = sess.query(User).first() + sess = Session() + u1 = sess.query(User).first() - from sqlalchemy.orm import Query + from sqlalchemy.orm import Query - canary = mock.Mock() + canary = mock.Mock() - # I would think Mock can do this but apparently - # it cannot (wrap / autospec don't work together) - real_compile_context = Query._compile_context + # I would think Mock can do this but apparently + # it cannot (wrap / autospec don't work together) + real_compile_context = Query._compile_context - def _my_compile_context(*arg, **kw): - if arg[0].column_descriptions[0]['entity'] is Address: - canary() - return real_compile_context(*arg, **kw) + def _my_compile_context(*arg, **kw): + if arg[0].column_descriptions[0]['entity'] is Address: + canary() + return real_compile_context(*arg, **kw) - with mock.patch.object( - Query, - "_compile_context", - _my_compile_context - ): - u1.addresses + with mock.patch.object( + Query, + "_compile_context", + _my_compile_context + ): + u1.addresses - sess.expire(u1) - u1.addresses - finally: - baked.unbake_lazy_loaders() + sess.expire(u1) + u1.addresses if flag: eq_(canary.call_count, 1) @@ -907,7 +878,7 @@ class LazyLoaderTest(testing.AssertsCompiledSQL, BakedTest): lambda s: s.query(User)) if set_option: - base_bq += lambda q: q.options(baked_lazyload(User.addresses)) + base_bq += lambda q: q.options(lazyload(User.addresses)) base_bq += lambda q: q.order_by(User.id) @@ -965,7 +936,7 @@ class LazyLoaderTest(testing.AssertsCompiledSQL, BakedTest): base_bq = self.bakery( lambda s: s.query(Address)) - base_bq += lambda q: q.options(baked_lazyload(Address.user)) + base_bq += lambda q: q.options(lazyload(Address.user)) base_bq += lambda q: q.order_by(Address.id) assert_result = self.static.address_user_result diff --git a/test/orm/test_options.py b/test/orm/test_options.py index b7c574e2a5..e29a63e08d 100644 --- a/test/orm/test_options.py +++ b/test/orm/test_options.py @@ -2,7 +2,7 @@ from sqlalchemy import inspect from sqlalchemy.orm import attributes, mapper, relationship, backref, \ configure_mappers, create_session, synonym, Session, class_mapper, \ aliased, column_property, joinedload_all, joinedload, Query,\ - util as orm_util, Load, defer + util as orm_util, Load, defer, defaultload from sqlalchemy.orm.query import QueryContext from sqlalchemy.orm import strategy_options import sqlalchemy as sa @@ -20,6 +20,11 @@ class QueryTest(_fixtures.FixtureTest): def setup_mappers(cls): cls._setup_stock_mapping() + class SubItem(cls.classes.Item): + pass + + mapper(SubItem, None, inherits=cls.classes.Item) + class PathTest(object): def _make_path(self, path): @@ -42,7 +47,9 @@ class PathTest(object): attr = {} for val in opt._to_bind: - val._bind_loader(q, attr, False) + val._bind_loader( + [ent.entity_zero for ent in q._mapper_entities], + q._current_path, attr, False) assert_paths = [k[1] for k in attr] eq_( @@ -868,7 +875,9 @@ class LocalOptsTest(PathTest, QueryTest): for opt in opts: if isinstance(opt, strategy_options._UnboundLoad): for tb in opt._to_bind: - tb._bind_loader(query, attr, False) + tb._bind_loader( + [ent.entity_zero for ent in query._mapper_entities], + query._current_path, attr, False) else: attr.update(opt.context) @@ -951,3 +960,387 @@ class LocalOptsTest(PathTest, QueryTest): ), ] self._assert_attrs(opts, {"foo": "bar", "bat": "hoho"}) + + +class CacheKeyTest(PathTest, QueryTest): + + run_create_tables = False + run_inserts = None + run_deletes = None + + def test_unbound_cache_key_included_safe(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "orders"]) + + opt = joinedload(User.orders).joinedload(Order.items) + eq_( + opt._generate_cache_key(query_path), + ( + ((Order, 'items', Item, ('lazy', 'joined')),) + ) + ) + + def test_bound_cache_key_included_safe(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "orders"]) + + opt = Load(User).joinedload(User.orders).joinedload(Order.items) + eq_( + opt._generate_cache_key(query_path), + ( + ((Order, 'items', Item, ('lazy', 'joined')),) + ) + ) + + def test_unbound_cache_key_excluded_on_other(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry( + [User, "addresses"]) + + opt = joinedload(User.orders).joinedload(Order.items) + eq_( + opt._generate_cache_key(query_path), + None + ) + + def test_bound_cache_key_excluded_on_other(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry( + [User, "addresses"]) + + opt = Load(User).joinedload(User.orders).joinedload(Order.items) + eq_( + opt._generate_cache_key(query_path), + None + ) + + def test_unbound_cache_key_excluded_on_aliased(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + # query of: + # + # query(User).options( + # joinedload(aliased(User).orders).joinedload(Order.items)) + # + # we are lazy loading Order objects from User.orders + # the path excludes our option so cache key should + # be None + + query_path = self._make_path_registry( + [User, "orders"]) + + opt = joinedload(aliased(User).orders).joinedload(Order.items) + eq_( + opt._generate_cache_key(query_path), + None + ) + + def test_unbound_cache_key_included_of_type_safe(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "orders"]) + + opt = joinedload(User.orders).joinedload(Order.items.of_type(SubItem)) + eq_( + opt._generate_cache_key(query_path), + ( + (Order, 'items', SubItem, ('lazy', 'joined')), + ) + ) + + def test_bound_cache_key_included_of_type_safe(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "orders"]) + + opt = Load(User).joinedload(User.orders).\ + joinedload(Order.items.of_type(SubItem)) + + eq_( + opt._generate_cache_key(query_path), + ( + (Order, 'items', SubItem, ('lazy', 'joined')), + ) + ) + + def test_unbound_cache_key_included_unsafe_option_one(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "orders"]) + + opt = joinedload(User.orders).\ + joinedload(Order.items.of_type(aliased(SubItem))) + eq_( + opt._generate_cache_key(query_path), + False + ) + + def test_unbound_cache_key_included_unsafe_option_two(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "orders", Order]) + + opt = joinedload(User.orders).\ + joinedload(Order.items.of_type(aliased(SubItem))) + eq_( + opt._generate_cache_key(query_path), + False + ) + + def test_unbound_cache_key_included_unsafe_option_three(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "orders", Order, "items"]) + + opt = joinedload(User.orders).\ + joinedload(Order.items.of_type(aliased(SubItem))) + eq_( + opt._generate_cache_key(query_path), + False + ) + + def test_unbound_cache_key_included_unsafe_query(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + au = aliased(User) + query_path = self._make_path_registry([inspect(au), "orders"]) + + opt = joinedload(au.orders).\ + joinedload(Order.items) + eq_( + opt._generate_cache_key(query_path), + False + ) + + def test_unbound_cache_key_included_safe_w_deferred(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "addresses"]) + + opt = joinedload(User.addresses).\ + defer(Address.email_address).defer(Address.user_id) + eq_( + opt._generate_cache_key(query_path), + ( + ( + Address, "email_address", + ('deferred', True), + ('instrument', True) + ), + ( + Address, "user_id", + ('deferred', True), + ('instrument', True) + ), + ) + ) + + def test_bound_cache_key_included_safe_w_deferred(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "addresses"]) + + opt = Load(User).joinedload(User.addresses).\ + defer(Address.email_address).defer(Address.user_id) + eq_( + opt._generate_cache_key(query_path), + ( + ( + Address, "email_address", + ('deferred', True), + ('instrument', True) + ), + ( + Address, "user_id", + ('deferred', True), + ('instrument', True) + ), + ) + ) + + def test_unbound_cache_key_included_safe_w_option(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + opt = defaultload("orders").joinedload( + "items", innerjoin=True).defer("description") + query_path = self._make_path_registry([User, "orders"]) + + eq_( + opt._generate_cache_key(query_path), + ( + (Order, 'items', Item, + ('lazy', 'joined'), ('innerjoin', True)), + (Order, 'items', Item, 'description', + ('deferred', True), ('instrument', True)) + ) + ) + + def test_bound_cache_key_excluded_on_aliased(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry( + [User, "orders"]) + + au = aliased(User) + opt = Load(au).joinedload(au.orders).joinedload(Order.items) + eq_( + opt._generate_cache_key(query_path), + None + ) + + def test_bound_cache_key_included_unsafe_option_one(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "orders"]) + + opt = Load(User).joinedload(User.orders).\ + joinedload(Order.items.of_type(aliased(SubItem))) + eq_( + opt._generate_cache_key(query_path), + False + ) + + def test_bound_cache_key_included_unsafe_option_two(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "orders", Order]) + + opt = Load(User).joinedload(User.orders).\ + joinedload(Order.items.of_type(aliased(SubItem))) + eq_( + opt._generate_cache_key(query_path), + False + ) + + def test_bound_cache_key_included_unsafe_option_three(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "orders", Order, "items"]) + + opt = Load(User).joinedload(User.orders).\ + joinedload(Order.items.of_type(aliased(SubItem))) + eq_( + opt._generate_cache_key(query_path), + False + ) + + def test_bound_cache_key_included_unsafe_query(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + au = aliased(User) + query_path = self._make_path_registry([inspect(au), "orders"]) + + opt = Load(au).joinedload(au.orders).\ + joinedload(Order.items) + eq_( + opt._generate_cache_key(query_path), + False + ) + + + def test_bound_cache_key_included_safe_w_option(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + opt = Load(User).defaultload("orders").joinedload( + "items", innerjoin=True).defer("description") + query_path = self._make_path_registry([User, "orders"]) + + eq_( + opt._generate_cache_key(query_path), + ( + (Order, 'items', Item, + ('lazy', 'joined'), ('innerjoin', True)), + (Order, 'items', Item, 'description', + ('deferred', True), ('instrument', True)) + ) + ) + + def test_unbound_cache_key_included_safe_w_loadonly_strs(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "addresses"]) + + opt = defaultload(User.addresses).load_only("id", "email_address") + eq_( + opt._generate_cache_key(query_path), + + ( + (Address, 'id', + ('deferred', False), ('instrument', True)), + (Address, 'email_address', + ('deferred', False), ('instrument', True)), + (Address, 'column:*', + ('deferred', True), ('instrument', True), + ('undefer_pks', True)) + ) + ) + + def test_unbound_cache_key_included_safe_w_loadonly_props(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "addresses"]) + + opt = defaultload(User.addresses).load_only( + Address.id, Address.email_address) + eq_( + opt._generate_cache_key(query_path), + + ( + (Address, 'id', + ('deferred', False), ('instrument', True)), + (Address, 'email_address', + ('deferred', False), ('instrument', True)), + (Address, 'column:*', + ('deferred', True), ('instrument', True), + ('undefer_pks', True)) + ) + ) + + def test_bound_cache_key_included_safe_w_loadonly(self): + User, Address, Order, Item, SubItem = self.classes( + 'User', 'Address', 'Order', 'Item', 'SubItem') + + query_path = self._make_path_registry([User, "addresses"]) + + opt = Load(User).defaultload(User.addresses).\ + load_only("id", "email_address") + eq_( + opt._generate_cache_key(query_path), + + ( + (Address, 'id', + ('deferred', False), ('instrument', True)), + (Address, 'email_address', + ('deferred', False), ('instrument', True)), + (Address, 'column:*', + ('deferred', True), ('instrument', True), + ('undefer_pks', True)) + ) + ) + diff --git a/test/profiles.txt b/test/profiles.txt index 720c26b80d..8af5f9b1b4 100644 --- a/test/profiles.txt +++ b/test/profiles.txt @@ -211,60 +211,60 @@ test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline 3.5_sqlite_pysqlite_d # TEST: test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols -test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_mysql_mysqldb_dbapiunicode_cextensions 23231 -test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_mysql_mysqldb_dbapiunicode_nocextensions 26236 -test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_postgresql_psycopg2_dbapiunicode_cextensions 23198 -test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 26203 -test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_sqlite_pysqlite_dbapiunicode_cextensions 23175 -test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 26180 +test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_mysql_mysqldb_dbapiunicode_cextensions 23237 +test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_mysql_mysqldb_dbapiunicode_nocextensions 26242 +test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_postgresql_psycopg2_dbapiunicode_cextensions 23204 +test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 26209 +test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_sqlite_pysqlite_dbapiunicode_cextensions 23181 +test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 26186 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.4_mysql_mysqldb_dbapiunicode_cextensions 24260 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.4_mysql_mysqldb_dbapiunicode_nocextensions 27267 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.4_postgresql_psycopg2_dbapiunicode_cextensions 24225 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.4_postgresql_psycopg2_dbapiunicode_nocextensions 27232 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.4_sqlite_pysqlite_dbapiunicode_cextensions 24211 test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.4_sqlite_pysqlite_dbapiunicode_nocextensions 27218 -test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.5_mysql_mysqldb_dbapiunicode_cextensions 24253 -test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.5_mysql_mysqldb_dbapiunicode_nocextensions 27260 -test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.5_postgresql_psycopg2_dbapiunicode_cextensions 24222 -test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 27229 -test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.5_sqlite_pysqlite_dbapiunicode_cextensions 24204 -test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 27211 +test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.5_mysql_mysqldb_dbapiunicode_cextensions 24259 +test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.5_mysql_mysqldb_dbapiunicode_nocextensions 27266 +test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.5_postgresql_psycopg2_dbapiunicode_cextensions 24228 +test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 27235 +test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.5_sqlite_pysqlite_dbapiunicode_cextensions 24210 +test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 27217 # TEST: test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 2.7_mysql_mysqldb_dbapiunicode_cextensions 406489 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 2.7_mysql_mysqldb_dbapiunicode_nocextensions 406475 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 2.7_postgresql_psycopg2_dbapiunicode_cextensions 406489 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 406468 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 2.7_sqlite_pysqlite_dbapiunicode_cextensions 406482 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 406482 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 2.7_mysql_mysqldb_dbapiunicode_cextensions 413452 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 2.7_mysql_mysqldb_dbapiunicode_nocextensions 413431 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 2.7_postgresql_psycopg2_dbapiunicode_cextensions 413445 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 413438 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 2.7_sqlite_pysqlite_dbapiunicode_cextensions 413438 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 413438 test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.4_mysql_mysqldb_dbapiunicode_cextensions 532398 test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.4_mysql_mysqldb_dbapiunicode_nocextensions 532398 test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.4_postgresql_psycopg2_dbapiunicode_cextensions 532398 test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.4_postgresql_psycopg2_dbapiunicode_nocextensions 532398 test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.4_sqlite_pysqlite_dbapiunicode_cextensions 532398 test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.4_sqlite_pysqlite_dbapiunicode_nocextensions 532398 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.5_mysql_mysqldb_dbapiunicode_cextensions 434497 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.5_mysql_mysqldb_dbapiunicode_nocextensions 434504 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.5_postgresql_psycopg2_dbapiunicode_cextensions 434504 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 434490 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.5_sqlite_pysqlite_dbapiunicode_cextensions 434497 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 434504 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.5_mysql_mysqldb_dbapiunicode_cextensions 440781 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.5_mysql_mysqldb_dbapiunicode_nocextensions 440774 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.5_postgresql_psycopg2_dbapiunicode_cextensions 440774 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 440781 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.5_sqlite_pysqlite_dbapiunicode_cextensions 440788 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_build_query 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 440781 # TEST: test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 2.7_mysql_mysqldb_dbapiunicode_cextensions 567666 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 2.7_mysql_mysqldb_dbapiunicode_nocextensions 580466 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 2.7_postgresql_psycopg2_dbapiunicode_cextensions 578324 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 591124 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 2.7_sqlite_pysqlite_dbapiunicode_cextensions 557938 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 570738 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 3.5_mysql_mysqldb_dbapiunicode_cextensions 570849 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 3.5_mysql_mysqldb_dbapiunicode_nocextensions 583849 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 3.5_postgresql_psycopg2_dbapiunicode_cextensions 598107 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 611107 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 3.5_sqlite_pysqlite_dbapiunicode_cextensions 562621 -test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 575621 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 2.7_mysql_mysqldb_dbapiunicode_cextensions 567786 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 2.7_mysql_mysqldb_dbapiunicode_nocextensions 580586 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 2.7_postgresql_psycopg2_dbapiunicode_cextensions 578444 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 591244 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 2.7_sqlite_pysqlite_dbapiunicode_cextensions 558058 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 570760 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 3.5_mysql_mysqldb_dbapiunicode_cextensions 570897 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 3.5_mysql_mysqldb_dbapiunicode_nocextensions 583897 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 3.5_postgresql_psycopg2_dbapiunicode_cextensions 598155 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 611155 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 3.5_sqlite_pysqlite_dbapiunicode_cextensions 562571 +test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 575669 # TEST: test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_identity @@ -289,66 +289,66 @@ test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_ # TEST: test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity -test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_mysql_mysqldb_dbapiunicode_cextensions 137052 -test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_mysql_mysqldb_dbapiunicode_nocextensions 139302 -test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_postgresql_psycopg2_dbapiunicode_cextensions 127552 -test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 129802 -test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_sqlite_pysqlite_dbapiunicode_cextensions 125302 -test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 127552 +test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_mysql_mysqldb_dbapiunicode_cextensions 94694 +test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_mysql_mysqldb_dbapiunicode_nocextensions 96493 +test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_postgresql_psycopg2_dbapiunicode_cextensions 85692 +test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 87444 +test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_sqlite_pysqlite_dbapiunicode_cextensions 83193 +test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 84945 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.4_mysql_mysqldb_dbapiunicode_cextensions 143106 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.4_mysql_mysqldb_dbapiunicode_nocextensions 145856 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.4_postgresql_psycopg2_dbapiunicode_cextensions 133056 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.4_postgresql_psycopg2_dbapiunicode_nocextensions 135806 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.4_sqlite_pysqlite_dbapiunicode_cextensions 131306 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.4_sqlite_pysqlite_dbapiunicode_nocextensions 134056 -test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.5_mysql_mysqldb_dbapiunicode_cextensions 143054 -test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.5_mysql_mysqldb_dbapiunicode_nocextensions 145854 -test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.5_postgresql_psycopg2_dbapiunicode_cextensions 134604 -test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 137304 -test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.5_sqlite_pysqlite_dbapiunicode_cextensions 131304 -test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 134054 +test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.5_mysql_mysqldb_dbapiunicode_cextensions 97011 +test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.5_mysql_mysqldb_dbapiunicode_nocextensions 98765 +test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.5_postgresql_psycopg2_dbapiunicode_cextensions 89009 +test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 90763 +test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.5_sqlite_pysqlite_dbapiunicode_cextensions 85510 +test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 87264 # TEST: test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks -test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_mysql_mysqldb_dbapiunicode_cextensions 19549 -test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_mysql_mysqldb_dbapiunicode_nocextensions 19797 -test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_postgresql_psycopg2_dbapiunicode_cextensions 19028 -test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 19264 -test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_sqlite_pysqlite_dbapiunicode_cextensions 18904 -test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 19122 +test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_mysql_mysqldb_dbapiunicode_cextensions 18601 +test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_mysql_mysqldb_dbapiunicode_nocextensions 18872 +test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_postgresql_psycopg2_dbapiunicode_cextensions 18080 +test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 18257 +test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_sqlite_pysqlite_dbapiunicode_cextensions 17950 +test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 18126 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.4_mysql_mysqldb_dbapiunicode_cextensions 20210 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.4_mysql_mysqldb_dbapiunicode_nocextensions 20428 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.4_postgresql_psycopg2_dbapiunicode_cextensions 19528 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.4_postgresql_psycopg2_dbapiunicode_nocextensions 19834 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.4_sqlite_pysqlite_dbapiunicode_cextensions 19494 test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.4_sqlite_pysqlite_dbapiunicode_nocextensions 19700 -test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.5_mysql_mysqldb_dbapiunicode_cextensions 20152 -test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.5_mysql_mysqldb_dbapiunicode_nocextensions 20465 -test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.5_postgresql_psycopg2_dbapiunicode_cextensions 19641 -test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 19921 -test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.5_sqlite_pysqlite_dbapiunicode_cextensions 19412 -test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 19687 +test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.5_mysql_mysqldb_dbapiunicode_cextensions 19155 +test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.5_mysql_mysqldb_dbapiunicode_nocextensions 19399 +test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.5_postgresql_psycopg2_dbapiunicode_cextensions 18573 +test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 18805 +test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.5_sqlite_pysqlite_dbapiunicode_cextensions 18382 +test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 18627 # TEST: test.aaa_profiling.test_orm.MergeTest.test_merge_load -test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_mysql_mysqldb_dbapiunicode_cextensions 1446 -test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_mysql_mysqldb_dbapiunicode_nocextensions 1465 -test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_postgresql_psycopg2_dbapiunicode_cextensions 1330 -test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 1349 -test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_sqlite_pysqlite_dbapiunicode_cextensions 1213 -test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 1231 +test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_mysql_mysqldb_dbapiunicode_cextensions 1190 +test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_mysql_mysqldb_dbapiunicode_nocextensions 1207 +test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_postgresql_psycopg2_dbapiunicode_cextensions 1083 +test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 1100 +test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_sqlite_pysqlite_dbapiunicode_cextensions 958 +test.aaa_profiling.test_orm.MergeTest.test_merge_load 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 975 test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.4_mysql_mysqldb_dbapiunicode_cextensions 1494 test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.4_mysql_mysqldb_dbapiunicode_nocextensions 1517 test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.4_postgresql_psycopg2_dbapiunicode_cextensions 1365 test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.4_postgresql_psycopg2_dbapiunicode_nocextensions 1388 test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.4_sqlite_pysqlite_dbapiunicode_cextensions 1257 test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.4_sqlite_pysqlite_dbapiunicode_nocextensions 1280 -test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.5_mysql_mysqldb_dbapiunicode_cextensions 1495 -test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.5_mysql_mysqldb_dbapiunicode_nocextensions 1519 -test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.5_postgresql_psycopg2_dbapiunicode_cextensions 1375 -test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 1398 -test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.5_sqlite_pysqlite_dbapiunicode_cextensions 1256 -test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 1280 +test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.5_mysql_mysqldb_dbapiunicode_cextensions 1226 +test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.5_mysql_mysqldb_dbapiunicode_nocextensions 1245 +test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.5_postgresql_psycopg2_dbapiunicode_cextensions 1114 +test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 1133 +test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.5_sqlite_pysqlite_dbapiunicode_cextensions 988 +test.aaa_profiling.test_orm.MergeTest.test_merge_load 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 1007 # TEST: test.aaa_profiling.test_orm.MergeTest.test_merge_no_load @@ -390,28 +390,28 @@ test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.5_mysql_mysqldb_dbapiuni test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.5_postgresql_psycopg2_dbapiunicode_cextensions 6324 test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 6894 test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.5_sqlite_pysqlite_dbapiunicode_cextensions 6096 -test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 6756 +test.aaa_profiling.test_orm.QueryTest.test_query_cols 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 6666 # TEST: test.aaa_profiling.test_orm.SessionTest.test_expire_lots -test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_mysql_mysqldb_dbapiunicode_cextensions 1147 -test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_mysql_mysqldb_dbapiunicode_nocextensions 1152 -test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_postgresql_psycopg2_dbapiunicode_cextensions 1148 -test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 1134 -test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_sqlite_pysqlite_dbapiunicode_cextensions 1155 -test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 1152 +test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_mysql_mysqldb_dbapiunicode_cextensions 1138 +test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_mysql_mysqldb_dbapiunicode_nocextensions 1156 +test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_postgresql_psycopg2_dbapiunicode_cextensions 1147 +test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 1140 +test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_sqlite_pysqlite_dbapiunicode_cextensions 1154 +test.aaa_profiling.test_orm.SessionTest.test_expire_lots 2.7_sqlite_pysqlite_dbapiunicode_nocextensions 1168 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.4_mysql_mysqldb_dbapiunicode_cextensions 1256 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.4_mysql_mysqldb_dbapiunicode_nocextensions 1252 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.4_postgresql_psycopg2_dbapiunicode_cextensions 1235 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.4_postgresql_psycopg2_dbapiunicode_nocextensions 1278 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.4_sqlite_pysqlite_dbapiunicode_cextensions 1258 test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.4_sqlite_pysqlite_dbapiunicode_nocextensions 1252 -test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.5_mysql_mysqldb_dbapiunicode_cextensions 1264 -test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.5_mysql_mysqldb_dbapiunicode_nocextensions 1254 -test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.5_postgresql_psycopg2_dbapiunicode_cextensions 1250 -test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 1265 -test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.5_sqlite_pysqlite_dbapiunicode_cextensions 1235 -test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 1242 +test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.5_mysql_mysqldb_dbapiunicode_cextensions 1302 +test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.5_mysql_mysqldb_dbapiunicode_nocextensions 1258 +test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.5_postgresql_psycopg2_dbapiunicode_cextensions 1257 +test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 1259 +test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.5_sqlite_pysqlite_dbapiunicode_cextensions 1286 +test.aaa_profiling.test_orm.SessionTest.test_expire_lots 3.5_sqlite_pysqlite_dbapiunicode_nocextensions 1251 # TEST: test.aaa_profiling.test_pool.QueuePoolTest.test_first_connect @@ -623,8 +623,8 @@ test.aaa_profiling.test_zoomark.ZooMarkTest.test_invocation 3.5_postgresql_psyco # TEST: test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 2.7_postgresql_psycopg2_dbapiunicode_cextensions 6462,410,6777,17745,1170,2657 -test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 6552,415,6897,18814,1276,2692 +test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 2.7_postgresql_psycopg2_dbapiunicode_nocextensions 6558,415,6897,18814,1276,2692 test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 3.4_postgresql_psycopg2_dbapiunicode_cextensions 6430,401,6925,18147,1169,2705 test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 3.4_postgresql_psycopg2_dbapiunicode_nocextensions 6520,406,7061,19258,1271,2746 -test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 3.5_postgresql_psycopg2_dbapiunicode_cextensions 6430,401,6909,18147,1169,2705 +test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 3.5_postgresql_psycopg2_dbapiunicode_cextensions 6435,401,6909,18147,1169,2705 test.aaa_profiling.test_zoomark_orm.ZooMarkTest.test_invocation 3.5_postgresql_psycopg2_dbapiunicode_nocextensions 6527,406,7045,19258,1271,2746 -- 2.47.2