summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/sql/elements.py
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2020-03-09 17:12:35 -0400
committerMike Bayer <mike_mp@zzzcomputing.com>2020-04-01 16:12:23 -0400
commita9b62055bfa61c11e9fe0b2984437e2c3e32bf0e (patch)
tree366027c7069edd56d49e9d540ae6a14fbe9e16fe /lib/sqlalchemy/sql/elements.py
parente6250123a30e457068878394e49b7ca07ca4d3b0 (diff)
downloadsqlalchemy-a9b62055bfa61c11e9fe0b2984437e2c3e32bf0e.tar.gz
Try to measure new style caching in the ORM, take two
Supercedes: If78fbb557c6f2cae637799c3fec2cbc5ac248aaf Trying to see if by making the cache key memoized, we still can have the older "identity" form of caching which is the cheapest of all, at the same time as the newer "cache key each time" version that is not nearly as cheap; but still much cheaper than no caching at all. Also needed is a per-execution update of _keymap when we invoke from a cached select, so that Column objects that are anonymous or otherwise adapted will match up. this is analogous to the adaption of bound parameters from the cache key. Adds test coverage for the keymap / construct_params() changes related to caching. Also hones performance to a large extent for statement construction and cache key generation. Also includes a new memoized attribute approach that vastly simplifies the previous approach of "group_expirable_memoized_property" and finally integrates cleanly with _clone(), _generate(), etc. no more hardcoding of attributes is needed, as well as that most _reset_memoization() calls are no longer needed as the reset is inherent in a _generate() call; this also has dramatic performance improvements. Change-Id: I95c560ffcbfa30b26644999412fb6a385125f663
Diffstat (limited to 'lib/sqlalchemy/sql/elements.py')
-rw-r--r--lib/sqlalchemy/sql/elements.py73
1 files changed, 34 insertions, 39 deletions
diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py
index 2b994c513..57d41b06f 100644
--- a/lib/sqlalchemy/sql/elements.py
+++ b/lib/sqlalchemy/sql/elements.py
@@ -175,7 +175,7 @@ def not_(clause):
@inspection._self_inspects
class ClauseElement(
- roles.SQLRole, SupportsWrappingAnnotations, HasCacheKey, Traversible
+ roles.SQLRole, SupportsWrappingAnnotations, HasCacheKey, Traversible,
):
"""Base class for elements of a programmatically constructed SQL
expression.
@@ -215,10 +215,9 @@ class ClauseElement(
the _copy_internals() method.
"""
+ skip = self._memoized_keys
c = self.__class__.__new__(self.__class__)
- c.__dict__ = self.__dict__.copy()
- ClauseElement._cloned_set._reset(c)
- ColumnElement.comparator._reset(c)
+ c.__dict__ = {k: v for k, v in self.__dict__.items() if k not in skip}
# this is a marker that helps to "equate" clauses to each other
# when a Select returns its list of FROM clauses. the cloning
@@ -250,7 +249,7 @@ class ClauseElement(
"""
return self.__class__
- @util.memoized_property
+ @HasMemoized.memoized_attribute
def _cloned_set(self):
"""Return the set consisting all cloned ancestors of this
ClauseElement.
@@ -276,6 +275,7 @@ class ClauseElement(
def __getstate__(self):
d = self.__dict__.copy()
d.pop("_is_clone_of", None)
+ d.pop("_generate_cache_key", None)
return d
def _execute_on_connection(self, connection, multiparams, params):
@@ -740,15 +740,7 @@ class ColumnElement(
def type(self):
return type_api.NULLTYPE
- def _with_binary_element_type(self, type_):
- cloned = self._clone()
- cloned._copy_internals(
- clone=lambda element: element._with_binary_element_type(type_)
- )
- cloned.type = type_
- return cloned
-
- @util.memoized_property
+ @HasMemoized.memoized_attribute
def comparator(self):
try:
comparator_factory = self.type.comparator_factory
@@ -1022,6 +1014,7 @@ class BindParameter(roles.InElementRole, ColumnElement):
_is_crud = False
_expanding_in_types = ()
_is_bind_parameter = True
+ _key_is_anon = False
def __init__(
self,
@@ -1273,9 +1266,6 @@ class BindParameter(roles.InElementRole, ColumnElement):
"""
- if isinstance(key, ColumnClause):
- type_ = key.type
- key = key.key
if required is NO_ARG:
required = value is NO_ARG and callable_ is None
if value is NO_ARG:
@@ -1297,8 +1287,12 @@ class BindParameter(roles.InElementRole, ColumnElement):
else "param",
)
)
+ self._key_is_anon = True
+ elif key:
+ self.key = key
else:
- self.key = key or _anonymous_label("%%(%d param)s" % id(self))
+ self.key = _anonymous_label("%%(%d param)s" % id(self))
+ self._key_is_anon = True
# identifying key that won't change across
# clones, used to identify the bind's logical
@@ -1366,6 +1360,11 @@ class BindParameter(roles.InElementRole, ColumnElement):
else:
return self.value
+ def _with_binary_element_type(self, type_):
+ c = ClauseElement._clone(self)
+ c.type = type_
+ return c
+
def _clone(self):
c = ClauseElement._clone(self)
if self.unique:
@@ -1390,7 +1389,7 @@ class BindParameter(roles.InElementRole, ColumnElement):
id_,
self.__class__,
self.type._static_cache_key,
- traversals._resolve_name_for_compare(self, self.key, anon_map),
+ self.key % anon_map if self._key_is_anon else self.key,
)
def _convert_to_unique(self):
@@ -2790,7 +2789,7 @@ class Cast(WrapsColumnExpression, ColumnElement):
return self.clause
-class TypeCoerce(HasMemoized, WrapsColumnExpression, ColumnElement):
+class TypeCoerce(WrapsColumnExpression, ColumnElement):
"""Represent a Python-side type-coercion wrapper.
:class:`.TypeCoerce` supplies the :func:`.expression.type_coerce`
@@ -2815,8 +2814,6 @@ class TypeCoerce(HasMemoized, WrapsColumnExpression, ColumnElement):
("type", InternalTraversal.dp_type),
]
- _memoized_property = util.group_expirable_memoized_property()
-
def __init__(self, expression, type_):
r"""Associate a SQL expression with a particular type, without rendering
``CAST``.
@@ -2889,7 +2886,7 @@ class TypeCoerce(HasMemoized, WrapsColumnExpression, ColumnElement):
def _from_objects(self):
return self.clause._from_objects
- @_memoized_property
+ @HasMemoized.memoized_attribute
def typed_expression(self):
if isinstance(self.clause, BindParameter):
bp = self.clause._clone()
@@ -3435,7 +3432,7 @@ class BinaryExpression(ColumnElement):
# refer to BinaryExpression directly and pass strings
if isinstance(operator, util.string_types):
operator = operators.custom_op(operator)
- self._orig = (hash(left), hash(right))
+ self._orig = (left.__hash__(), right.__hash__())
self.left = left.self_group(against=operator)
self.right = right.self_group(against=operator)
self.operator = operator
@@ -3450,7 +3447,7 @@ class BinaryExpression(ColumnElement):
def __bool__(self):
if self.operator in (operator.eq, operator.ne):
- return self.operator(self._orig[0], self._orig[1])
+ return self.operator(*self._orig)
else:
raise TypeError("Boolean value of this clause is not defined")
@@ -3546,6 +3543,9 @@ class Grouping(GroupedElement, ColumnElement):
self.element = element
self.type = getattr(element, "type", type_api.NULLTYPE)
+ def _with_binary_element_type(self, type_):
+ return Grouping(self.element._with_binary_element_type(type_))
+
@util.memoized_property
def _is_implicitly_boolean(self):
return self.element._is_implicitly_boolean
@@ -4015,7 +4015,7 @@ class FunctionFilter(ColumnElement):
)
-class Label(HasMemoized, roles.LabeledColumnExprRole, ColumnElement):
+class Label(roles.LabeledColumnExprRole, ColumnElement):
"""Represents a column label (AS).
Represent a label, as typically applied to any column-level
@@ -4031,8 +4031,6 @@ class Label(HasMemoized, roles.LabeledColumnExprRole, ColumnElement):
("_element", InternalTraversal.dp_clauseelement),
]
- _memoized_property = util.group_expirable_memoized_property()
-
def __init__(self, name, element, type_=None):
"""Return a :class:`Label` object for the
given :class:`.ColumnElement`.
@@ -4075,7 +4073,7 @@ class Label(HasMemoized, roles.LabeledColumnExprRole, ColumnElement):
def _is_implicitly_boolean(self):
return self.element._is_implicitly_boolean
- @_memoized_property
+ @HasMemoized.memoized_attribute
def _allow_label_resolve(self):
return self.element._allow_label_resolve
@@ -4089,7 +4087,7 @@ class Label(HasMemoized, roles.LabeledColumnExprRole, ColumnElement):
self._type or getattr(self._element, "type", None)
)
- @_memoized_property
+ @HasMemoized.memoized_attribute
def element(self):
return self._element.self_group(against=operators.as_)
@@ -4116,7 +4114,6 @@ class Label(HasMemoized, roles.LabeledColumnExprRole, ColumnElement):
return self.element.foreign_keys
def _copy_internals(self, clone=_clone, anonymize_labels=False, **kw):
- self._reset_memoizations()
self._element = clone(self._element, **kw)
if anonymize_labels:
self.name = self._resolve_label = _anonymous_label(
@@ -4194,8 +4191,6 @@ class ColumnClause(
_is_multiparam_column = False
- _memoized_property = util.group_expirable_memoized_property()
-
def __init__(self, text, type_=None, is_literal=False, _selectable=None):
"""Produce a :class:`.ColumnClause` object.
@@ -4312,7 +4307,7 @@ class ColumnClause(
else:
return []
- @_memoized_property
+ @HasMemoized.memoized_attribute
def _from_objects(self):
t = self.table
if t is not None:
@@ -4327,18 +4322,18 @@ class ColumnClause(
else:
return self.name.encode("ascii", "backslashreplace")
- @_memoized_property
+ @HasMemoized.memoized_attribute
def _key_label(self):
if self.key != self.name:
return self._gen_label(self.key)
else:
return self._label
- @_memoized_property
+ @HasMemoized.memoized_attribute
def _label(self):
return self._gen_label(self.name)
- @_memoized_property
+ @HasMemoized.memoized_attribute
def _render_label_in_columns_clause(self):
return self.table is not None
@@ -4599,14 +4594,14 @@ def _corresponding_column_or_error(fromclause, column, require_embedded=False):
class AnnotatedColumnElement(Annotated):
def __init__(self, element, values):
Annotated.__init__(self, element, values)
- ColumnElement.comparator._reset(self)
+ self.__dict__.pop("comparator", None)
for attr in ("name", "key", "table"):
if self.__dict__.get(attr, False) is None:
self.__dict__.pop(attr)
def _with_annotations(self, values):
clone = super(AnnotatedColumnElement, self)._with_annotations(values)
- ColumnElement.comparator._reset(clone)
+ clone.__dict__.pop("comparator", None)
return clone
@util.memoized_property