return c
+ def _cache_key(self, **kw):
+ """return an optional cache key.
+
+ The cache key is a tuple which can contain any series of
+ objects that are hashable and also identifies
+ this object uniquely within the presence of a larger SQL expression
+ or statement, for the purposes of caching the resulting query.
+
+ The cache key should be based on the SQL compiled structure that would
+ ultimately be produced. That is, two structures that are composed in
+ exactly the same way should produce the same cache key; any difference
+ in the strucures that would affect the SQL string or the type handlers
+ should result in a different cache key.
+
+ If a structure cannot produce a useful cache key, it should raise
+ NotImplementedError, which will result in the entire structure
+ for which it's part of not being useful as a cache key.
+
+
+ """
+ raise NotImplementedError(self.__class__)
+
@property
def _constructor(self):
"""return the 'constructor' for this ClauseElement.
else:
return comparator_factory(self)
+ def _cache_key(self, **kw):
+ raise NotImplementedError(self.__class__)
+
def __getattr__(self, key):
try:
return getattr(self.comparator, key)
if required is NO_ARG:
required = value is NO_ARG and callable_ is None
if value is NO_ARG:
+ self._value_required_for_cache = False
value = None
+ else:
+ self._value_required_for_cache = True
if quote is not None:
key = quoted_name(key, quote)
)
return c
+ def _cache_key(self, bindparams=None, **kw):
+ if bindparams is None:
+ # even though _cache_key is a private method, we would like to
+ # be super paranoid about this point. You can't include the
+ # "value" or "callable" in the cache key, because the value is
+ # not part of the structure of a statement and is likely to
+ # change every time. However you cannot *throw it away* either,
+ # because you can't invoke the statement without the parameter
+ # values that were explicitly placed. So require that they
+ # are collected here to make sure this happens.
+ if self._value_required_for_cache:
+ raise NotImplementedError(
+ "bindparams collection argument required for _cache_key "
+ "implementation. Bound parameter cache keys are not safe "
+ "to use without accommodating for the value or callable "
+ "within the parameter itself.")
+ else:
+ bindparams.append(self)
+ return (BindParameter, self.type._cache_key, self._orig_key)
+
def _convert_to_unique(self):
if not self.unique:
self.unique = True
def __init__(self, type_):
self.type = type_
+ def _cache_key(self, **kw):
+ return (TypeClause, self.type._cache_key)
+
class TextClause(Executable, ClauseElement):
"""Represent a literal SQL text fragment.
def get_children(self, **kwargs):
return list(self._bindparams.values())
+ def _cache_key(self, **kw):
+ return (self.text,) + tuple(
+ bind._cache_key for bind in self._bindparams.values()
+ )
+
class Null(ColumnElement):
"""Represent the NULL keyword in a SQL statement.
return Null()
+ def _cache_key(self, **kw):
+ return (Null,)
+
class False_(ColumnElement):
"""Represent the ``false`` keyword, or equivalent, in a SQL statement.
return False_()
+ def _cache_key(self, **kw):
+ return (False_,)
+
class True_(ColumnElement):
"""Represent the ``true`` keyword, or equivalent, in a SQL statement.
return True_()
+ def _cache_key(self, **kw):
+ return (True_,)
+
class ClauseList(ClauseElement):
"""Describe a list of clauses, separated by an operator.
def get_children(self, **kwargs):
return self.clauses
+ def _cache_key(self, **kw):
+ return (ClauseList, self.operator) + tuple(
+ clause._cache_key(**kw) for clause in self.clauses
+ )
+
@property
def _from_objects(self):
return list(itertools.chain(*[c._from_objects for c in self.clauses]))
"BooleanClauseList has a private constructor"
)
+ def _cache_key(self, **kw):
+ return (BooleanClauseList, self.operator) + tuple(
+ clause._cache_key(**kw) for clause in self.clauses
+ )
+
@classmethod
def _construct(cls, operator, continue_on, skip_on, *clauses, **kw):
convert_clauses = []
def _select_iterable(self):
return (self,)
+ def _cache_key(self, **kw):
+ return (Tuple,) + tuple(
+ clause._cache_key(**kw) for clause in self.clauses
+ )
+
def _bind_param(self, operator, obj, type_=None):
return Tuple(
*[
if self.else_ is not None:
yield self.else_
+ def _cache_key(self, **kw):
+ return (
+ (
+ Case,
+ self.value._cache_key(**kw)
+ if self.value is not None
+ else None,
+ )
+ + tuple(
+ (x._cache_key(**kw), y._cache_key(**kw)) for x, y in self.whens
+ )
+ + (
+ self.else_._cache_key(**kw)
+ if self.else_ is not None
+ else None,
+ )
+ )
+
@property
def _from_objects(self):
return list(
def get_children(self, **kwargs):
return self.clause, self.typeclause
+ def _cache_key(self, **kw):
+ return (
+ Cast,
+ self.clause._cache_key(**kw),
+ self.typeclause._cache_key(**kw),
+ )
+
@property
def _from_objects(self):
return self.clause._from_objects
def get_children(self, **kwargs):
return (self.clause,)
+ def _cache_key(self, **kw):
+ return (TypeCoerce, self.type._cache_key, self.clause._cache_key(**kw))
+
@property
def _from_objects(self):
return self.clause._from_objects
def get_children(self, **kwargs):
return (self.expr,)
+ def _cache_key(self, **kw):
+ return (Extract, self.field, self.expr._cache_key(**kw))
+
@property
def _from_objects(self):
return self.expr._from_objects
def _copy_internals(self, clone=_clone, **kw):
self.element = clone(self.element, **kw)
+ def _cache_key(self, **kw):
+ return (_label_reference, self.element._cache_key(**kw))
+
def get_children(self, **kwargs):
return [self.element]
def _text_clause(self):
return TextClause._create_text(self.element)
+ def _cache_key(self, **kw):
+ return (_textual_label_reference, self.element)
+
class UnaryExpression(ColumnElement):
"""Define a 'unary' expression.
def _copy_internals(self, clone=_clone, **kw):
self.element = clone(self.element, **kw)
+ def _cache_key(self, **kw):
+ return (
+ UnaryExpression,
+ self.element._cache_key(**kw),
+ self.operator,
+ self.modifier,
+ )
+
def get_children(self, **kwargs):
return (self.element,)
def self_group(self, against=None):
return self
+ def _cache_key(self, **kw):
+ return (
+ self.element._cache_key(**kw),
+ self.type._cache_key,
+ self.operator,
+ self.negate,
+ self.modifier,
+ )
+
def _negate(self):
if isinstance(self.element, (True_, False_)):
return self.element._negate()
def get_children(self, **kwargs):
return self.left, self.right
+ def _cache_key(self, **kw):
+ return (
+ BinaryExpression,
+ self.left._cache_key(**kw),
+ self.right._cache_key(**kw),
+ )
+
def self_group(self, against=None):
if operators.is_precedent(self.operator, against):
return Grouping(self)
assert against is operator.getitem
return self
+ def _cache_key(self, **kw):
+ return (Slice, self.start, self.stop, self.step)
+
class IndexExpression(BinaryExpression):
"""Represent the class of expressions that are like an "index" operation.
def get_children(self, **kwargs):
return (self.element,)
+ def _cache_key(self, **kw):
+ return (Grouping, self.element._cache_key(**kw))
+
@property
def _from_objects(self):
return self.element._from_objects
if c is not None
]
+ def _cache_key(self, **kw):
+ return (
+ (Over,)
+ + tuple(
+ e._cache_key(**kw) if e is not None else None
+ for e in (self.element, self.partition_by, self.order_by)
+ )
+ + (self.range_, self.rows)
+ )
+
def _copy_internals(self, clone=_clone, **kw):
self.element = clone(self.element, **kw)
if self.partition_by is not None:
def get_children(self, **kwargs):
return [c for c in (self.element, self.order_by) if c is not None]
+ def _cache_key(self, **kw):
+ return (
+ WithinGroup,
+ self.element._cache_key(**kw)
+ if self.element is not None
+ else None,
+ self.order_by._cache_key(**kw)
+ if self.order_by is not None
+ else None,
+ )
+
def _copy_internals(self, clone=_clone, **kw):
self.element = clone(self.element, **kw)
if self.order_by is not None:
if self.criterion is not None:
self.criterion = clone(self.criterion, **kw)
+ def _cache_key(self, **kw):
+ return (
+ FunctionFilter,
+ self.func._cache_key(**kw),
+ self.criterion._cache_key(**kw)
+ if self.criterion is not None
+ else None,
+ )
+
@property
def _from_objects(self):
return list(
def __reduce__(self):
return self.__class__, (self.name, self._element, self._type)
+ def _cache_key(self, **kw):
+ return (Label, self.element._cache_key(**kw), self._resolve_label)
+
@util.memoized_property
def _is_implicitly_boolean(self):
return self.element._is_implicitly_boolean
table = property(_get_table, _set_table)
+ def _cache_key(self, **kw):
+ return (
+ self.name,
+ self.table.name if self.table is not None else None,
+ self.is_literal,
+ self.type._cache_key,
+ )
+
@_memoized_property
def _from_objects(self):
t = self.table
def __init__(self, collation):
self.collation = collation
+ def _cache_key(self, **kw):
+ return (CollationClause, self.collation)
+
class _IdentifiedClause(Executable, ClauseElement):
def get_children(self, **kwargs):
return self.left, self.right, self.onclause
+ def _cache_key(self, **kw):
+ return (
+ Join,
+ self.isouter,
+ self.full,
+ self.left._cache_key(**kw),
+ self.right._cache_key(**kw),
+ self.onclause._cache_key(**kw),
+ )
+
def _match_primaries(self, left, right):
if isinstance(left, Join):
left_right = left.right
if self.supports_execution:
self._execution_options = baseselectable._execution_options
self.element = selectable
+ self._orig_name = name
if name is None:
if self.original.named_with_column:
name = getattr(self.original, "name", None)
yield c
yield self.element
+ def _cache_key(self, **kw):
+ return (self.__class__, self.element._cache_key(**kw), self._orig_name)
+
@property
def _from_objects(self):
return [self]
def _copy_internals(self, clone=_clone, **kw):
self.element = clone(self.element, **kw)
+ def _cache_key(self, **kw):
+ return (FromGrouping, self.element._cache_key(**kw))
+
@property
def _from_objects(self):
return self.element._from_objects
else:
return []
+ def _cache_key(self, **kw):
+ return (TableClause, self.name) + tuple(
+ col._cache_key(**kw) for col in self._columns
+ )
+
@util.dependencies("sqlalchemy.sql.dml")
def insert(self, dml, values=None, inline=False, **kwargs):
"""Generate an :func:`.insert` construct against this
if self.of is not None:
self.of = [clone(col, **kw) for col in self.of]
+ def _cache_key(self, **kw):
+ return (
+ ForUpdateArg,
+ self.nowait,
+ self.read,
+ self.skip_locked,
+ self.of._cache_key(**kw) if self.of is not None else None,
+ )
+
def __init__(
self,
nowait=False,
+ list(self.selects)
)
+ def _cache_key(self, **kw):
+ return (
+ (CompoundSelect, self.keyword)
+ + tuple(stmt._cache_key(**kw) for stmt in self.selects)
+ + (
+ self._order_by_clause._cache_key(**kw)
+ if self._order_by_clause is not None
+ else None,
+ )
+ + (
+ self._group_by_clause._cache_key(**kw)
+ if self._group_by_clause is not None
+ else None,
+ )
+ + (
+ self._for_update_arg._cache_key(**kw)
+ if self._for_update_arg is not None
+ else None,
+ )
+ )
+
def bind(self):
if self._bind:
return self._bind
]
)
+ def _cache_key(self, **kw):
+ return (
+ (Select,)
+ + ("raw_columns",)
+ + tuple(elem._cache_key(**kw) for elem in self._raw_columns)
+ + ("elements",)
+ + tuple(
+ elem._cache_key(**kw) if elem is not None else None
+ for elem in (
+ self._whereclause,
+ self._having,
+ self._order_by_clause,
+ self._group_by_clause,
+ )
+ )
+ + ("from_obj",)
+ + tuple(elem._cache_key(**kw) for elem in self._from_obj)
+ + ("correlate",)
+ + tuple(
+ elem._cache_key(**kw)
+ for elem in (
+ self._correlate if self._correlate is not None else ()
+ )
+ )
+ + ("correlate_except",)
+ + tuple(
+ elem._cache_key(**kw)
+ for elem in (
+ self._correlate_except
+ if self._correlate_except is not None
+ else ()
+ )
+ )
+ + ("for_update",),
+ (
+ self._for_update_arg._cache_key(**kw)
+ if self._for_update_arg is not None
+ else None,
+ ),
+ )
+
@_generative
def column(self, column):
"""return a new select() construct with the given column expression
yield c
yield self.element
+ def _cache_key(self, **kw):
+ return (TextAsFrom, self.element._cache_key(**kw)) + tuple(
+ col._cache_key(**kw) for col in self.column_args
+ )
+
def _scalar_type(self):
return self.column_args[0].type
from sqlalchemy.sql import operators
from sqlalchemy.sql import True_
from sqlalchemy.sql import type_coerce
+from sqlalchemy.sql import visitors
from sqlalchemy.sql.elements import _label_reference
from sqlalchemy.sql.elements import _textual_label_reference
from sqlalchemy.sql.elements import Annotated
from sqlalchemy.sql.selectable import _OffsetLimitParam
from sqlalchemy.sql.selectable import FromGrouping
from sqlalchemy.sql.selectable import Selectable
+from sqlalchemy.testing import assert_raises_message
+from sqlalchemy.testing import eq_
from sqlalchemy.testing import fixtures
+from sqlalchemy.testing import is_
from sqlalchemy.testing import is_false
from sqlalchemy.testing import is_true
+from sqlalchemy.testing import ne_
from sqlalchemy.util import class_hierarchy
"%r == %r" % (case_a[a], case_b[b]),
)
+ def test_cache_key(self):
+ def assert_params_append(assert_params):
+ def append(param):
+ if param._value_required_for_cache:
+ assert_params.append(param)
+ else:
+ is_(param.value, None)
+
+ return append
+
+ for fixture in self.fixtures:
+ case_a = fixture()
+ case_b = fixture()
+
+ for a, b in itertools.combinations_with_replacement(
+ range(len(case_a)), 2
+ ):
+
+ assert_a_params = []
+ assert_b_params = []
+
+ visitors.traverse_depthfirst(
+ case_a[a],
+ {},
+ {"bindparam": assert_params_append(assert_a_params)},
+ )
+ visitors.traverse_depthfirst(
+ case_b[b],
+ {},
+ {"bindparam": assert_params_append(assert_b_params)},
+ )
+ if assert_a_params:
+ assert_raises_message(
+ NotImplementedError,
+ "bindparams collection argument required ",
+ case_a[a]._cache_key,
+ )
+ if assert_b_params:
+ assert_raises_message(
+ NotImplementedError,
+ "bindparams collection argument required ",
+ case_b[b]._cache_key,
+ )
+
+ if not assert_a_params and not assert_b_params:
+ if a == b:
+ eq_(case_a[a]._cache_key(), case_b[b]._cache_key())
+ else:
+ ne_(case_a[a]._cache_key(), case_b[b]._cache_key())
+
+ def test_cache_key_gather_bindparams(self):
+ for fixture in self.fixtures:
+ case_a = fixture()
+ case_b = fixture()
+
+ # in the "bindparams" case, the cache keys for bound parameters
+ # with only different values will be the same, but the params
+ # themselves are gathered into a collection.
+ for a, b in itertools.combinations_with_replacement(
+ range(len(case_a)), 2
+ ):
+ a_params = {"bindparams": []}
+ b_params = {"bindparams": []}
+ if a == b:
+ a_key = case_a[a]._cache_key(**a_params)
+ b_key = case_b[b]._cache_key(**b_params)
+ eq_(a_key, b_key)
+
+ if a_params["bindparams"]:
+ for a_param, b_param in zip(
+ a_params["bindparams"], b_params["bindparams"]
+ ):
+ assert a_param.compare(b_param)
+ else:
+ a_key = case_a[a]._cache_key(**a_params)
+ b_key = case_b[b]._cache_key(**b_params)
+
+ if a_key == b_key:
+ for a_param, b_param in zip(
+ a_params["bindparams"], b_params["bindparams"]
+ ):
+ if not a_param.compare(b_param):
+ break
+ else:
+ assert False, "Bound parameters are all the same"
+ else:
+ ne_(a_key, b_key)
+
+ assert_a_params = []
+ assert_b_params = []
+ visitors.traverse_depthfirst(
+ case_a[a], {}, {"bindparam": assert_a_params.append}
+ )
+ visitors.traverse_depthfirst(
+ case_b[b], {}, {"bindparam": assert_b_params.append}
+ )
+
+ # note we're asserting the order of the params as well as
+ # if there are dupes or not. ordering has to be deterministic
+ # and matches what a traversal would provide.
+ eq_(a_params["bindparams"], assert_a_params)
+ eq_(b_params["bindparams"], assert_b_params)
+
def test_compare_col_identity(self):
stmt1 = (
select([table_a.c.a, table_b.c.b])