summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2019-12-14 11:39:06 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2019-12-14 14:28:01 -0500
commit89bf6d80a999eb31ee4a69b229b887fbfb2ed12a (patch)
tree745ee0e0b913642b7e2cff2025727825efde7f5a
parentb63bf945fbb5eb1cc299fe9a5e0d92754626abd4 (diff)
downloadsqlalchemy-89bf6d80a999eb31ee4a69b229b887fbfb2ed12a.tar.gz
Traversal and clause generation performance improvements
Added one traversal test, callcounts have been brought from 29754 to 5173 so far. Change-Id: I164e9831600709ee214c1379bb215fdad73b39aa
-rw-r--r--lib/sqlalchemy/orm/mapper.py6
-rw-r--r--lib/sqlalchemy/sql/annotation.py37
-rw-r--r--lib/sqlalchemy/sql/elements.py47
-rw-r--r--lib/sqlalchemy/sql/functions.py3
-rw-r--r--lib/sqlalchemy/sql/selectable.py10
-rw-r--r--lib/sqlalchemy/sql/traversals.py229
-rw-r--r--lib/sqlalchemy/sql/type_api.py2
-rw-r--r--lib/sqlalchemy/sql/visitors.py19
-rw-r--r--test/aaa_profiling/test_misc.py79
-rw-r--r--test/profiles.txt16
10 files changed, 293 insertions, 155 deletions
diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py
index 548eca58d..aa350c7ba 100644
--- a/lib/sqlalchemy/orm/mapper.py
+++ b/lib/sqlalchemy/orm/mapper.py
@@ -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):
diff --git a/lib/sqlalchemy/sql/annotation.py b/lib/sqlalchemy/sql/annotation.py
index 0d995ec8a..9853cef2a 100644
--- a/lib/sqlalchemy/sql/annotation.py
+++ b/lib/sqlalchemy/sql/annotation.py
@@ -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
diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py
index eda31dc61..da7568330 100644
--- a/lib/sqlalchemy/sql/elements.py
+++ b/lib/sqlalchemy/sql/elements.py
@@ -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
):
diff --git a/lib/sqlalchemy/sql/functions.py b/lib/sqlalchemy/sql/functions.py
index 96e64dc28..ac409ee0b 100644
--- a/lib/sqlalchemy/sql/functions.py
+++ b/lib/sqlalchemy/sql/functions.py
@@ -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
diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py
index 0eadab610..5f609f8fd 100644
--- a/lib/sqlalchemy/sql/selectable.py
+++ b/lib/sqlalchemy/sql/selectable.py
@@ -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",
diff --git a/lib/sqlalchemy/sql/traversals.py b/lib/sqlalchemy/sql/traversals.py
index c0782ce48..588bbc3dc 100644
--- a/lib/sqlalchemy/sql/traversals.py
+++ b/lib/sqlalchemy/sql/traversals.py
@@ -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
):
diff --git a/lib/sqlalchemy/sql/type_api.py b/lib/sqlalchemy/sql/type_api.py
index d09bb28bb..f4b873ecf 100644
--- a/lib/sqlalchemy/sql/type_api.py
+++ b/lib/sqlalchemy/sql/type_api.py
@@ -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])
diff --git a/lib/sqlalchemy/sql/visitors.py b/lib/sqlalchemy/sql/visitors.py
index 8c06eb8af..dcded3484 100644
--- a/lib/sqlalchemy/sql/visitors.py
+++ b/lib/sqlalchemy/sql/visitors.py
@@ -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.
diff --git a/test/aaa_profiling/test_misc.py b/test/aaa_profiling/test_misc.py
index c2b6f3d08..fdb40c2e6 100644
--- a/test/aaa_profiling/test_misc.py
+++ b/test/aaa_profiling/test_misc.py
@@ -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
diff --git a/test/profiles.txt b/test/profiles.txt
index 721ce9677..493f27698 100644
--- a/test/profiles.txt
+++ b/test/profiles.txt
@@ -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