]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Traversal and clause generation performance improvements
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 14 Dec 2019 16:39:06 +0000 (11:39 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sat, 14 Dec 2019 19:28:01 +0000 (14:28 -0500)
Added one traversal test, callcounts have been brought from 29754 to
5173 so far.

Change-Id: I164e9831600709ee214c1379bb215fdad73b39aa

lib/sqlalchemy/orm/mapper.py
lib/sqlalchemy/sql/annotation.py
lib/sqlalchemy/sql/elements.py
lib/sqlalchemy/sql/functions.py
lib/sqlalchemy/sql/selectable.py
lib/sqlalchemy/sql/traversals.py
lib/sqlalchemy/sql/type_api.py
lib/sqlalchemy/sql/visitors.py
test/aaa_profiling/test_misc.py
test/profiles.txt

index 548eca58db7485985661bea302aedbd5309ad439..aa350c7baa55322953a8629b695dddaa639156d4 100644 (file)
@@ -2209,8 +2209,12 @@ class Mapper(sql_base.HasCacheKey, InspectionAttr):
             for table, columns in self._cols_by_table.items()
         )
 
+    # temporarily commented out until we fix an issue in the serializer
+    #    @_memoized_configured_property.method
     def __clause_element__(self):
-        return self.selectable
+        return self.selectable  # ._annotate(
+        # {"parententity": self, "parentmapper": self}
+        # )
 
     @property
     def selectable(self):
index 0d995ec8a23ad956e980ae3076e4a14a94460149..9853cef2acc9d05cf4aa71929c82dcfb9d3710e3 100644 (file)
@@ -19,23 +19,26 @@ from .. import util
 
 class SupportsAnnotations(object):
     @util.memoized_property
-    def _annotation_traversals(self):
-        return [
-            (
-                key,
-                InternalTraversal.dp_has_cache_key
-                if isinstance(value, HasCacheKey)
-                else InternalTraversal.dp_plain_obj,
-            )
-            for key, value in self._annotations.items()
-        ]
+    def _annotations_cache_key(self):
+        return (
+            "_annotations",
+            tuple(
+                (
+                    key,
+                    value._gen_cache_key(None, [])
+                    if isinstance(value, HasCacheKey)
+                    else value,
+                )
+                for key, value in self._annotations.items()
+            ),
+        )
 
 
 class SupportsCloneAnnotations(SupportsAnnotations):
     _annotations = util.immutabledict()
 
     _traverse_internals = [
-        ("_annotations", InternalTraversal.dp_annotations_state)
+        ("_annotations_cache_key", InternalTraversal.dp_plain_obj)
     ]
 
     def _annotate(self, values):
@@ -45,7 +48,7 @@ class SupportsCloneAnnotations(SupportsAnnotations):
         """
         new = self._clone()
         new._annotations = new._annotations.union(values)
-        new.__dict__.pop("_annotation_traversals", None)
+        new.__dict__.pop("_annotations_cache_key", None)
         return new
 
     def _with_annotations(self, values):
@@ -55,7 +58,7 @@ class SupportsCloneAnnotations(SupportsAnnotations):
         """
         new = self._clone()
         new._annotations = util.immutabledict(values)
-        new.__dict__.pop("_annotation_traversals", None)
+        new.__dict__.pop("_annotations_cache_key", None)
         return new
 
     def _deannotate(self, values=None, clone=False):
@@ -71,7 +74,7 @@ class SupportsCloneAnnotations(SupportsAnnotations):
             # the expression for a deep deannotation
             new = self._clone()
             new._annotations = {}
-            new.__dict__.pop("_annotation_traversals", None)
+            new.__dict__.pop("_annotations_cache_key", None)
             return new
         else:
             return self
@@ -146,7 +149,7 @@ class Annotated(object):
 
     def __init__(self, element, values):
         self.__dict__ = element.__dict__.copy()
-        self.__dict__.pop("_annotation_traversals", None)
+        self.__dict__.pop("_annotations_cache_key", None)
         self.__element = element
         self._annotations = values
         self._hash = hash(element)
@@ -159,7 +162,7 @@ class Annotated(object):
     def _with_annotations(self, values):
         clone = self.__class__.__new__(self.__class__)
         clone.__dict__ = self.__dict__.copy()
-        clone.__dict__.pop("_annotation_traversals", None)
+        clone.__dict__.pop("_annotations_cache_key", None)
         clone._annotations = values
         return clone
 
@@ -305,7 +308,7 @@ def _new_annotation_type(cls, base_cls):
 
     if "_traverse_internals" in cls.__dict__:
         anno_cls._traverse_internals = list(cls._traverse_internals) + [
-            ("_annotations", InternalTraversal.dp_annotations_state)
+            ("_annotations_cache_key", InternalTraversal.dp_plain_obj)
         ]
     return anno_cls
 
index eda31dc6199aa7732c35b141a0068aa4301ee38b..da75683305c0f6c64e0882e8016cafceaeaebf32 100644 (file)
@@ -198,12 +198,7 @@ class ClauseElement(
 
     _order_by_label_element = None
 
-    @property
-    def _cache_key_traversal(self):
-        try:
-            return self._traverse_internals
-        except AttributeError:
-            return NO_CACHE
+    _cache_key_traversal = None
 
     def _clone(self):
         """Create a shallow copy of this ClauseElement.
@@ -1344,16 +1339,21 @@ class BindParameter(roles.InElementRole, ColumnElement):
         return c
 
     def _gen_cache_key(self, anon_map, bindparams):
-        if self in anon_map:
-            return (anon_map[self], self.__class__)
+        idself = id(self)
+        if idself in anon_map:
+            return (anon_map[idself], self.__class__)
+        else:
+            # inline of
+            # id_ = anon_map[idself]
+            anon_map[idself] = id_ = str(anon_map.index)
+            anon_map.index += 1
 
-        id_ = anon_map[self]
         bindparams.append(self)
 
         return (
             id_,
             self.__class__,
-            self.type._gen_cache_key,
+            self.type._static_cache_key,
             traversals._resolve_name_for_compare(self, self.key, anon_map),
         )
 
@@ -3239,6 +3239,33 @@ class BinaryExpression(ColumnElement):
 
     """
 
+    def _gen_cache_key(self, anon_map, bindparams):
+        # inlined for performance
+
+        idself = id(self)
+
+        if idself in anon_map:
+            return (anon_map[idself], self.__class__)
+        else:
+            # inline of
+            # id_ = anon_map[idself]
+            anon_map[idself] = id_ = str(anon_map.index)
+            anon_map.index += 1
+
+        if self._cache_key_traversal is NO_CACHE:
+            anon_map[NO_CACHE] = True
+            return None
+
+        result = (id_, self.__class__)
+
+        return result + (
+            ("left", self.left._gen_cache_key(anon_map, bindparams)),
+            ("right", self.right._gen_cache_key(anon_map, bindparams)),
+            ("operator", self.operator),
+            ("negate", self.negate),
+            ("modifiers", self.modifiers),
+        )
+
     def __init__(
         self, left, right, operator, type_=None, negate=None, modifiers=None
     ):
index 96e64dc284f56039f37f364fdffdbce9eba83d56..ac409ee0b26486f2cb344e0cdb88e418d174e138 100644 (file)
@@ -404,6 +404,9 @@ class FunctionAsBinary(BinaryExpression):
         ("modifiers", InternalTraversal.dp_plain_dict),
     ]
 
+    def _gen_cache_key(self, anon_map, bindparams):
+        return ColumnElement._gen_cache_key(self, anon_map, bindparams)
+
     def __init__(self, fn, left_index, right_index):
         self.sql_function = fn
         self.left_index = left_index
index 0eadab610708a85d29f7418623fea238625bb194..5f609f8fdcfb861ba6807bc1356948f7273e691f 100644 (file)
@@ -3148,8 +3148,14 @@ class Select(
             ("_raw_columns", InternalTraversal.dp_clauseelement_list),
             ("_whereclause", InternalTraversal.dp_clauseelement),
             ("_having", InternalTraversal.dp_clauseelement),
-            ("_order_by_clause", InternalTraversal.dp_clauseelement_list),
-            ("_group_by_clause", InternalTraversal.dp_clauseelement_list),
+            (
+                "_order_by_clause.clauses",
+                InternalTraversal.dp_clauseelement_list,
+            ),
+            (
+                "_group_by_clause.clauses",
+                InternalTraversal.dp_clauseelement_list,
+            ),
             ("_correlate", InternalTraversal.dp_clauseelement_unordered_set),
             (
                 "_correlate_except",
index c0782ce48671f043c803c49503ae3ede4c0430c9..588bbc3dcfddf6a74243689b3019ebb9547923ea 100644 (file)
@@ -1,5 +1,6 @@
 from collections import deque
 from collections import namedtuple
+import operator
 
 from . import operators
 from .visitors import ExtendedInternalTraversal
@@ -11,6 +12,9 @@ SKIP_TRAVERSE = util.symbol("skip_traverse")
 COMPARE_FAILED = False
 COMPARE_SUCCEEDED = True
 NO_CACHE = util.symbol("no_cache")
+CACHE_IN_PLACE = util.symbol("cache_in_place")
+CALL_GEN_CACHE_KEY = util.symbol("call_gen_cache_key")
+STATIC_CACHE_KEY = util.symbol("static_cache_key")
 
 
 def compare(obj1, obj2, **kw):
@@ -46,22 +50,82 @@ class HasCacheKey(object):
 
         """
 
-        if self in anon_map:
-            return (anon_map[self], self.__class__)
+        idself = id(self)
 
-        id_ = anon_map[self]
-
-        if self._cache_key_traversal is NO_CACHE:
-            anon_map[NO_CACHE] = True
+        if anon_map is not None:
+            if idself in anon_map:
+                return (anon_map[idself], self.__class__)
+            else:
+                # inline of
+                # id_ = anon_map[idself]
+                anon_map[idself] = id_ = str(anon_map.index)
+                anon_map.index += 1
+        else:
+            id_ = None
+
+        _cache_key_traversal = self._cache_key_traversal
+        if _cache_key_traversal is None:
+            try:
+                _cache_key_traversal = self._traverse_internals
+            except AttributeError:
+                _cache_key_traversal = NO_CACHE
+
+        if _cache_key_traversal is NO_CACHE:
+            if anon_map is not None:
+                anon_map[NO_CACHE] = True
             return None
 
         result = (id_, self.__class__)
 
-        for attrname, obj, meth in _cache_key_traversal.run_generated_dispatch(
-            self, self._cache_key_traversal, "_generated_cache_key_traversal"
+        # inline of _cache_key_traversal_visitor.run_generated_dispatch()
+        try:
+            dispatcher = self.__class__.__dict__[
+                "_generated_cache_key_traversal"
+            ]
+        except KeyError:
+            dispatcher = _cache_key_traversal_visitor.generate_dispatch(
+                self, _cache_key_traversal, "_generated_cache_key_traversal"
+            )
+
+        for attrname, obj, meth in dispatcher(
+            self, _cache_key_traversal_visitor
         ):
             if obj is not None:
-                result += meth(attrname, obj, self, anon_map, bindparams)
+                if meth is CACHE_IN_PLACE:
+                    # cache in place is always going to be a Python
+                    # tuple, dict, list, etc. so we can do a boolean check
+                    if obj:
+                        result += (attrname, obj)
+                elif meth is STATIC_CACHE_KEY:
+                    result += (attrname, obj._static_cache_key)
+                elif meth is CALL_GEN_CACHE_KEY:
+                    result += (
+                        attrname,
+                        obj._gen_cache_key(anon_map, bindparams),
+                    )
+                elif meth is InternalTraversal.dp_clauseelement_list:
+                    if obj:
+                        result += (
+                            attrname,
+                            tuple(
+                                [
+                                    elem._gen_cache_key(anon_map, bindparams)
+                                    for elem in obj
+                                ]
+                            ),
+                        )
+                else:
+                    # note that all the "ClauseElement" standalone cases
+                    # here have been handled by inlines above; so we can
+                    # safely assume the object is a standard list/tuple/dict
+                    # which we can skip if it evaluates to false.
+                    # improvement would be to have this as a flag delivered
+                    # up front in the dispatcher list
+                    if obj:
+                        result += meth(
+                            attrname, obj, self, anon_map, bindparams
+                        )
+
         return result
 
     def _generate_cache_key(self):
@@ -118,17 +182,22 @@ def _clone(element, **kw):
 
 
 class _CacheKey(ExtendedInternalTraversal):
-    def visit_has_cache_key(self, attrname, obj, parent, anon_map, bindparams):
-        return (attrname, obj._gen_cache_key(anon_map, bindparams))
+    # very common elements are inlined into the main _get_cache_key() method
+    # to produce a dramatic savings in Python function call overhead
+
+    visit_has_cache_key = visit_clauseelement = CALL_GEN_CACHE_KEY
+    visit_clauseelement_list = InternalTraversal.dp_clauseelement_list
+    visit_string = (
+        visit_boolean
+    ) = visit_operator = visit_plain_obj = CACHE_IN_PLACE
+    visit_statement_hint_list = CACHE_IN_PLACE
+    visit_type = STATIC_CACHE_KEY
 
     def visit_inspectable(self, attrname, obj, parent, anon_map, bindparams):
         return self.visit_has_cache_key(
             attrname, inspect(obj), parent, anon_map, bindparams
         )
 
-    def visit_clauseelement(self, attrname, obj, parent, anon_map, bindparams):
-        return (attrname, obj._gen_cache_key(anon_map, bindparams))
-
     def visit_multi(self, attrname, obj, parent, anon_map, bindparams):
         return (
             attrname,
@@ -151,6 +220,8 @@ class _CacheKey(ExtendedInternalTraversal):
     def visit_has_cache_key_tuples(
         self, attrname, obj, parent, anon_map, bindparams
     ):
+        if not obj:
+            return ()
         return (
             attrname,
             tuple(
@@ -165,6 +236,8 @@ class _CacheKey(ExtendedInternalTraversal):
     def visit_has_cache_key_list(
         self, attrname, obj, parent, anon_map, bindparams
     ):
+        if not obj:
+            return ()
         return (
             attrname,
             tuple(elem._gen_cache_key(anon_map, bindparams) for elem in obj),
@@ -177,14 +250,6 @@ class _CacheKey(ExtendedInternalTraversal):
             attrname, [inspect(o) for o in obj], parent, anon_map, bindparams
         )
 
-    def visit_clauseelement_list(
-        self, attrname, obj, parent, anon_map, bindparams
-    ):
-        return (
-            attrname,
-            tuple(elem._gen_cache_key(anon_map, bindparams) for elem in obj),
-        )
-
     def visit_clauseelement_tuples(
         self, attrname, obj, parent, anon_map, bindparams
     ):
@@ -204,14 +269,18 @@ class _CacheKey(ExtendedInternalTraversal):
     def visit_fromclause_ordered_set(
         self, attrname, obj, parent, anon_map, bindparams
     ):
+        if not obj:
+            return ()
         return (
             attrname,
-            tuple(elem._gen_cache_key(anon_map, bindparams) for elem in obj),
+            tuple([elem._gen_cache_key(anon_map, bindparams) for elem in obj]),
         )
 
     def visit_clauseelement_unordered_set(
         self, attrname, obj, parent, anon_map, bindparams
     ):
+        if not obj:
+            return ()
         cache_keys = [
             elem._gen_cache_key(anon_map, bindparams) for elem in obj
         ]
@@ -230,39 +299,40 @@ class _CacheKey(ExtendedInternalTraversal):
     def visit_prefix_sequence(
         self, attrname, obj, parent, anon_map, bindparams
     ):
+        if not obj:
+            return ()
         return (
             attrname,
             tuple(
-                (clause._gen_cache_key(anon_map, bindparams), strval)
-                for clause, strval in obj
+                [
+                    (clause._gen_cache_key(anon_map, bindparams), strval)
+                    for clause, strval in obj
+                ]
             ),
         )
 
-    def visit_statement_hint_list(
-        self, attrname, obj, parent, anon_map, bindparams
-    ):
-        return (attrname, obj)
-
     def visit_table_hint_list(
         self, attrname, obj, parent, anon_map, bindparams
     ):
+        if not obj:
+            return ()
+
         return (
             attrname,
             tuple(
-                (
-                    clause._gen_cache_key(anon_map, bindparams),
-                    dialect_name,
-                    text,
-                )
-                for (clause, dialect_name), text in obj.items()
+                [
+                    (
+                        clause._gen_cache_key(anon_map, bindparams),
+                        dialect_name,
+                        text,
+                    )
+                    for (clause, dialect_name), text in obj.items()
+                ]
             ),
         )
 
-    def visit_type(self, attrname, obj, parent, anon_map, bindparams):
-        return (attrname, obj._gen_cache_key)
-
     def visit_plain_dict(self, attrname, obj, parent, anon_map, bindparams):
-        return (attrname, tuple((key, obj[key]) for key in sorted(obj)))
+        return (attrname, tuple([(key, obj[key]) for key in sorted(obj)]))
 
     def visit_string_clauseelement_dict(
         self, attrname, obj, parent, anon_map, bindparams
@@ -291,18 +361,6 @@ class _CacheKey(ExtendedInternalTraversal):
             ),
         )
 
-    def visit_string(self, attrname, obj, parent, anon_map, bindparams):
-        return (attrname, obj)
-
-    def visit_boolean(self, attrname, obj, parent, anon_map, bindparams):
-        return (attrname, obj)
-
-    def visit_operator(self, attrname, obj, parent, anon_map, bindparams):
-        return (attrname, obj)
-
-    def visit_plain_obj(self, attrname, obj, parent, anon_map, bindparams):
-        return (attrname, obj)
-
     def visit_fromclause_canonical_column_collection(
         self, attrname, obj, parent, anon_map, bindparams
     ):
@@ -311,22 +369,6 @@ class _CacheKey(ExtendedInternalTraversal):
             tuple(col._gen_cache_key(anon_map, bindparams) for col in obj),
         )
 
-    def visit_annotations_state(
-        self, attrname, obj, parent, anon_map, bindparams
-    ):
-        return (
-            attrname,
-            tuple(
-                (
-                    key,
-                    self.dispatch(sym)(
-                        key, obj[key], obj, anon_map, bindparams
-                    ),
-                )
-                for key, sym in parent._annotation_traversals
-            ),
-        )
-
     def visit_unknown_structure(
         self, attrname, obj, parent, anon_map, bindparams
     ):
@@ -334,7 +376,7 @@ class _CacheKey(ExtendedInternalTraversal):
         return ()
 
 
-_cache_key_traversal = _CacheKey()
+_cache_key_traversal_visitor = _CacheKey()
 
 
 class _CopyInternals(InternalTraversal):
@@ -489,29 +531,23 @@ class TraversalComparatorStrategy(InternalTraversal, util.MemoizedSlots):
                 right._traverse_internals,
                 fillvalue=(None, None),
             ):
+                if not compare_annotations and (
+                    (left_attrname == "_annotations_cache_key")
+                    or (right_attrname == "_annotations_cache_key")
+                ):
+                    continue
+
                 if (
                     left_attrname != right_attrname
                     or left_visit_sym is not right_visit_sym
                 ):
-                    if not compare_annotations and (
-                        (
-                            left_visit_sym
-                            is InternalTraversal.dp_annotations_state,
-                        )
-                        or (
-                            right_visit_sym
-                            is InternalTraversal.dp_annotations_state,
-                        )
-                    ):
-                        continue
-
                     return False
                 elif left_attrname in attributes_compared:
                     continue
 
                 dispatch = self.dispatch(left_visit_sym)
-                left_child = getattr(left, left_attrname)
-                right_child = getattr(right, right_attrname)
+                left_child = operator.attrgetter(left_attrname)(left)
+                right_child = operator.attrgetter(right_attrname)(right)
                 if left_child is None:
                     if right_child is not None:
                         return False
@@ -564,33 +600,6 @@ class TraversalComparatorStrategy(InternalTraversal, util.MemoizedSlots):
                 return COMPARE_FAILED
             self.stack.append((left[lstr], right[rstr]))
 
-    def visit_annotations_state(
-        self, left_parent, left, right_parent, right, **kw
-    ):
-        if not kw.get("compare_annotations", False):
-            return
-
-        for (lstr, lmeth), (rstr, rmeth) in util.zip_longest(
-            left_parent._annotation_traversals,
-            right_parent._annotation_traversals,
-            fillvalue=(None, None),
-        ):
-            if lstr != rstr or (lmeth is not rmeth):
-                return COMPARE_FAILED
-
-            dispatch = self.dispatch(lmeth)
-            left_child = left[lstr]
-            right_child = right[rstr]
-            if left_child is None:
-                if right_child is not None:
-                    return False
-                else:
-                    continue
-
-            comparison = dispatch(None, left_child, None, right_child, **kw)
-            if comparison is COMPARE_FAILED:
-                return comparison
-
     def visit_clauseelement_tuples(
         self, left_parent, left, right_parent, right, **kw
     ):
index d09bb28bbe69d279e8516cbba9d5ee287156248e..f4b873ecfa04f370647d3cedb31b192f5934d75a 100644 (file)
@@ -535,7 +535,7 @@ class TypeEngine(Traversible):
         return dialect.type_descriptor(self)
 
     @util.memoized_property
-    def _gen_cache_key(self):
+    def _static_cache_key(self):
         names = util.get_cls_kwargs(self.__class__)
         return (self.__class__,) + tuple(
             (k, self.__dict__[k])
index 8c06eb8afd7b2e395b80f2189203108b0024456a..dcded3484f098dff56c6ddb6d5d9251e67c83a3c 100644 (file)
@@ -216,12 +216,20 @@ class InternalTraversal(util.with_metaclass(_InternalTraversalType, object)):
         try:
             dispatcher = target.__class__.__dict__[generate_dispatcher_name]
         except KeyError:
-            dispatcher = _generate_dispatcher(
-                self, internal_dispatch, generate_dispatcher_name
+            dispatcher = self.generate_dispatch(
+                target, internal_dispatch, generate_dispatcher_name
             )
-            setattr(target.__class__, generate_dispatcher_name, dispatcher)
         return dispatcher(target, self)
 
+    def generate_dispatch(
+        self, target, internal_dispatch, generate_dispatcher_name
+    ):
+        dispatcher = _generate_dispatcher(
+            self, internal_dispatch, generate_dispatcher_name
+        )
+        setattr(target.__class__, generate_dispatcher_name, dispatcher)
+        return dispatcher
+
     dp_has_cache_key = symbol("HC")
     """Visit a :class:`.HasCacheKey` object."""
 
@@ -331,11 +339,6 @@ class InternalTraversal(util.with_metaclass(_InternalTraversalType, object)):
 
     """
 
-    dp_annotations_state = symbol("A")
-    """Visit the state of the :class:`.Annotatated` version of an object.
-
-    """
-
     dp_named_ddl_element = symbol("DD")
     """Visit a simple named DDL element.
 
index c2b6f3d08e87d48b7874a49c92677aca943cb761..fdb40c2e625817e3898bafcc00ae8e908bba4cc0 100644 (file)
@@ -1,4 +1,16 @@
+from sqlalchemy import Column
 from sqlalchemy import Enum
+from sqlalchemy import ForeignKey
+from sqlalchemy import Integer
+from sqlalchemy import MetaData
+from sqlalchemy import select
+from sqlalchemy import String
+from sqlalchemy import Table
+from sqlalchemy import testing
+from sqlalchemy.orm import join as ormjoin
+from sqlalchemy.orm import mapper
+from sqlalchemy.orm import relationship
+from sqlalchemy.testing import eq_
 from sqlalchemy.testing import fixtures
 from sqlalchemy.testing import profiling
 from sqlalchemy.util import classproperty
@@ -35,3 +47,70 @@ class EnumTest(fixtures.TestBase):
     @profiling.function_call_count()
     def test_create_enum_from_pep_435_w_expensive_members(self):
         Enum(self.SomeEnum)
+
+
+class CacheKeyTest(fixtures.TestBase):
+    __requires__ = ("cpython",)
+
+    @testing.fixture(scope="class")
+    def mapping_fixture(self):
+        # note in order to work nicely with "fixture" we are emerging
+        # a whole new model of setup/teardown, since pytest "fixture"
+        # sort of purposely works badly with setup/teardown
+
+        metadata = MetaData()
+        parent = Table(
+            "parent",
+            metadata,
+            Column("id", Integer, primary_key=True),
+            Column("data", String(20)),
+        )
+        child = Table(
+            "child",
+            metadata,
+            Column("id", Integer, primary_key=True),
+            Column("data", String(20)),
+            Column(
+                "parent_id", Integer, ForeignKey("parent.id"), nullable=False
+            ),
+        )
+
+        class Parent(testing.entities.BasicEntity):
+            pass
+
+        class Child(testing.entities.BasicEntity):
+            pass
+
+        mapper(
+            Parent,
+            parent,
+            properties={"children": relationship(Child, backref="parent")},
+        )
+        mapper(Child, child)
+
+        return Parent, Child
+
+    @testing.fixture(scope="function")
+    def stmt_fixture_one(self, mapping_fixture):
+        # note that by using ORM elements we will have annotations in these
+        # items also which is part of the performance hit
+        Parent, Child = mapping_fixture
+
+        return [
+            (
+                select([Parent.id, Child.id])
+                .select_from(ormjoin(Parent, Child, Parent.children))
+                .where(Child.id == 5)
+            )
+            for i in range(100)
+        ]
+
+    @profiling.function_call_count()
+    def test_statement_one(self, stmt_fixture_one):
+        current_key = None
+        for stmt in stmt_fixture_one:
+            key = stmt._generate_cache_key()
+            if current_key:
+                eq_(key, current_key)
+            else:
+                current_key = key
index 721ce9677c29930ce6fa124ccf5e5e35fb091647..493f2769833ffbe7324ed016453a9108e7bde1c7 100644 (file)
@@ -1,15 +1,15 @@
 # /home/classic/dev/sqlalchemy/test/profiles.txt
 # This file is written out on a per-environment basis.
-# For each test in aaa_profiling, the corresponding function and
+# For each test in aaa_profiling, the corresponding function and 
 # environment is located within this file.  If it doesn't exist,
 # the test is skipped.
-# If a callcount does exist, it is compared to what we received.
+# If a callcount does exist, it is compared to what we received. 
 # assertions are raised if the counts do not match.
-#
-# To add a new callcount test, apply the function_call_count
-# decorator and re-run the tests using the --write-profiles
+# 
+# To add a new callcount test, apply the function_call_count 
+# decorator and re-run the tests using the --write-profiles 
 # option - this file will be rewritten including the new count.
-#
+# 
 
 # TEST: test.aaa_profiling.test_compiler.CompileTest.test_insert
 
@@ -136,6 +136,10 @@ test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.7_postgre
 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.7_sqlite_pysqlite_dbapiunicode_cextensions 162
 test.aaa_profiling.test_compiler.CompileTest.test_update_whereclause 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 162
 
+# TEST: test.aaa_profiling.test_misc.CacheKeyTest.test_statement_one
+
+test.aaa_profiling.test_misc.CacheKeyTest.test_statement_one 3.7_sqlite_pysqlite_dbapiunicode_nocextensions 5173
+
 # TEST: test.aaa_profiling.test_misc.EnumTest.test_create_enum_from_pep_435_w_expensive_members
 
 test.aaa_profiling.test_misc.EnumTest.test_create_enum_from_pep_435_w_expensive_members 2.7_mssql_pyodbc_dbapiunicode_nocextensions 1325