1# sql/cache_key.py
2# Copyright (C) 2005-2025 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7
8from __future__ import annotations
9
10import enum
11from itertools import zip_longest
12import typing
13from typing import Any
14from typing import Callable
15from typing import Dict
16from typing import Final
17from typing import Iterable
18from typing import Iterator
19from typing import List
20from typing import Literal
21from typing import MutableMapping
22from typing import NamedTuple
23from typing import Optional
24from typing import Protocol
25from typing import Sequence
26from typing import Tuple
27from typing import Union
28
29from .visitors import anon_map
30from .visitors import HasTraversalDispatch
31from .visitors import HasTraverseInternals
32from .visitors import InternalTraversal
33from .visitors import prefix_anon_map
34from .. import util
35from ..inspection import inspect
36from ..util import HasMemoized
37
38if typing.TYPE_CHECKING:
39 from .elements import BindParameter
40 from .elements import ClauseElement
41 from .elements import ColumnElement
42 from .visitors import _TraverseInternalsType
43 from ..engine.interfaces import _CoreSingleExecuteParams
44
45
46class _CacheKeyTraversalDispatchType(Protocol):
47 def __call__(
48 s, self: HasCacheKey, visitor: _CacheKeyTraversal
49 ) -> _CacheKeyTraversalDispatchTypeReturn: ...
50
51
52class CacheConst(enum.Enum):
53 NO_CACHE = 0
54 PARAMS = 1
55
56
57NO_CACHE: Final = CacheConst.NO_CACHE
58
59
60_CacheKeyTraversalType = Union[
61 "_TraverseInternalsType", Literal[CacheConst.NO_CACHE], Literal[None]
62]
63
64
65class CacheTraverseTarget(enum.Enum):
66 CACHE_IN_PLACE = 0
67 CALL_GEN_CACHE_KEY = 1
68 STATIC_CACHE_KEY = 2
69 PROPAGATE_ATTRS = 3
70 ANON_NAME = 4
71
72
73(
74 CACHE_IN_PLACE,
75 CALL_GEN_CACHE_KEY,
76 STATIC_CACHE_KEY,
77 PROPAGATE_ATTRS,
78 ANON_NAME,
79) = tuple(CacheTraverseTarget)
80
81_CacheKeyTraversalDispatchTypeReturn = Sequence[
82 Tuple[
83 str,
84 Any,
85 Union[
86 Callable[..., Tuple[Any, ...]],
87 CacheTraverseTarget,
88 InternalTraversal,
89 ],
90 ]
91]
92
93
94class HasCacheKey:
95 """Mixin for objects which can produce a cache key.
96
97 This class is usually in a hierarchy that starts with the
98 :class:`.HasTraverseInternals` base, but this is optional. Currently,
99 the class should be able to work on its own without including
100 :class:`.HasTraverseInternals`.
101
102 .. seealso::
103
104 :class:`.CacheKey`
105
106 :ref:`sql_caching`
107
108 """
109
110 __slots__ = ()
111
112 _cache_key_traversal: _CacheKeyTraversalType = NO_CACHE
113
114 _is_has_cache_key = True
115
116 _hierarchy_supports_caching = True
117 """private attribute which may be set to False to prevent the
118 inherit_cache warning from being emitted for a hierarchy of subclasses.
119
120 Currently applies to the :class:`.ExecutableDDLElement` hierarchy which
121 does not implement caching.
122
123 """
124
125 inherit_cache: Optional[bool] = None
126 """Indicate if this :class:`.HasCacheKey` instance should make use of the
127 cache key generation scheme used by its immediate superclass.
128
129 The attribute defaults to ``None``, which indicates that a construct has
130 not yet taken into account whether or not its appropriate for it to
131 participate in caching; this is functionally equivalent to setting the
132 value to ``False``, except that a warning is also emitted.
133
134 This flag can be set to ``True`` on a particular class, if the SQL that
135 corresponds to the object does not change based on attributes which
136 are local to this class, and not its superclass.
137
138 .. seealso::
139
140 :ref:`compilerext_caching` - General guideslines for setting the
141 :attr:`.HasCacheKey.inherit_cache` attribute for third-party or user
142 defined SQL constructs.
143
144 """
145
146 __slots__ = ()
147
148 _generated_cache_key_traversal: Any
149
150 @classmethod
151 def _generate_cache_attrs(
152 cls,
153 ) -> Union[_CacheKeyTraversalDispatchType, Literal[CacheConst.NO_CACHE]]:
154 """generate cache key dispatcher for a new class.
155
156 This sets the _generated_cache_key_traversal attribute once called
157 so should only be called once per class.
158
159 """
160 inherit_cache = cls.__dict__.get("inherit_cache", None)
161 inherit = bool(inherit_cache)
162
163 if inherit:
164 _cache_key_traversal = getattr(cls, "_cache_key_traversal", None)
165 if _cache_key_traversal is None:
166 try:
167 assert issubclass(cls, HasTraverseInternals)
168 _cache_key_traversal = cls._traverse_internals
169 except AttributeError:
170 cls._generated_cache_key_traversal = NO_CACHE
171 return NO_CACHE
172
173 assert _cache_key_traversal is not NO_CACHE, (
174 f"class {cls} has _cache_key_traversal=NO_CACHE, "
175 "which conflicts with inherit_cache=True"
176 )
177
178 # TODO: wouldn't we instead get this from our superclass?
179 # also, our superclass may not have this yet, but in any case,
180 # we'd generate for the superclass that has it. this is a little
181 # more complicated, so for the moment this is a little less
182 # efficient on startup but simpler.
183 return _cache_key_traversal_visitor.generate_dispatch(
184 cls,
185 _cache_key_traversal,
186 "_generated_cache_key_traversal",
187 )
188 else:
189 _cache_key_traversal = cls.__dict__.get(
190 "_cache_key_traversal", None
191 )
192 if _cache_key_traversal is None:
193 _cache_key_traversal = cls.__dict__.get(
194 "_traverse_internals", None
195 )
196 if _cache_key_traversal is None:
197 cls._generated_cache_key_traversal = NO_CACHE
198 if (
199 inherit_cache is None
200 and cls._hierarchy_supports_caching
201 ):
202 util.warn(
203 "Class %s will not make use of SQL compilation "
204 "caching as it does not set the 'inherit_cache' "
205 "attribute to ``True``. This can have "
206 "significant performance implications including "
207 "some performance degradations in comparison to "
208 "prior SQLAlchemy versions. Set this attribute "
209 "to True if this object can make use of the cache "
210 "key generated by the superclass. Alternatively, "
211 "this attribute may be set to False which will "
212 "disable this warning." % (cls.__name__),
213 code="cprf",
214 )
215 return NO_CACHE
216
217 return _cache_key_traversal_visitor.generate_dispatch(
218 cls,
219 _cache_key_traversal,
220 "_generated_cache_key_traversal",
221 )
222
223 @util.preload_module("sqlalchemy.sql.elements")
224 def _gen_cache_key(
225 self, anon_map: anon_map, bindparams: List[BindParameter[Any]]
226 ) -> Optional[Tuple[Any, ...]]:
227 """return an optional cache key.
228
229 The cache key is a tuple which can contain any series of
230 objects that are hashable and also identifies
231 this object uniquely within the presence of a larger SQL expression
232 or statement, for the purposes of caching the resulting query.
233
234 The cache key should be based on the SQL compiled structure that would
235 ultimately be produced. That is, two structures that are composed in
236 exactly the same way should produce the same cache key; any difference
237 in the structures that would affect the SQL string or the type handlers
238 should result in a different cache key.
239
240 If a structure cannot produce a useful cache key, the NO_CACHE
241 symbol should be added to the anon_map and the method should
242 return None.
243
244 """
245
246 cls = self.__class__
247
248 id_, found = anon_map.get_anon(self)
249 if found:
250 return (id_, cls)
251
252 dispatcher: Union[
253 Literal[CacheConst.NO_CACHE],
254 _CacheKeyTraversalDispatchType,
255 ]
256
257 try:
258 dispatcher = cls.__dict__["_generated_cache_key_traversal"]
259 except KeyError:
260 # traversals.py -> _preconfigure_traversals()
261 # may be used to run these ahead of time, but
262 # is not enabled right now.
263 # this block will generate any remaining dispatchers.
264 dispatcher = cls._generate_cache_attrs()
265
266 if dispatcher is NO_CACHE:
267 anon_map[NO_CACHE] = True
268 return None
269
270 result: Tuple[Any, ...] = (id_, cls)
271
272 # inline of _cache_key_traversal_visitor.run_generated_dispatch()
273
274 for attrname, obj, meth in dispatcher(
275 self, _cache_key_traversal_visitor
276 ):
277 if obj is not None:
278 # TODO: see if C code can help here as Python lacks an
279 # efficient switch construct
280
281 if meth is STATIC_CACHE_KEY:
282 sck = obj._static_cache_key
283 if sck is NO_CACHE:
284 anon_map[NO_CACHE] = True
285 return None
286 result += (attrname, sck)
287 elif meth is ANON_NAME:
288 elements = util.preloaded.sql_elements
289 if isinstance(obj, elements._anonymous_label):
290 obj = obj.apply_map(anon_map) # type: ignore
291 result += (attrname, obj)
292 elif meth is CALL_GEN_CACHE_KEY:
293 result += (
294 attrname,
295 obj._gen_cache_key(anon_map, bindparams),
296 )
297
298 # remaining cache functions are against
299 # Python tuples, dicts, lists, etc. so we can skip
300 # if they are empty
301 elif obj:
302 if meth is CACHE_IN_PLACE:
303 result += (attrname, obj)
304 elif meth is PROPAGATE_ATTRS:
305 result += (
306 attrname,
307 obj["compile_state_plugin"],
308 (
309 obj["plugin_subject"]._gen_cache_key(
310 anon_map, bindparams
311 )
312 if obj["plugin_subject"]
313 else None
314 ),
315 )
316 elif meth is InternalTraversal.dp_annotations_key:
317 # obj is here is the _annotations dict. Table uses
318 # a memoized version of it. however in other cases,
319 # we generate it given anon_map as we may be from a
320 # Join, Aliased, etc.
321 # see #8790
322
323 if self._gen_static_annotations_cache_key: # type: ignore # noqa: E501
324 result += self._annotations_cache_key # type: ignore # noqa: E501
325 else:
326 result += self._gen_annotations_cache_key(anon_map) # type: ignore # noqa: E501
327
328 elif (
329 meth is InternalTraversal.dp_clauseelement_list
330 or meth is InternalTraversal.dp_clauseelement_tuple
331 or meth
332 is InternalTraversal.dp_memoized_select_entities
333 ):
334 result += (
335 attrname,
336 tuple(
337 [
338 elem._gen_cache_key(anon_map, bindparams)
339 for elem in obj
340 ]
341 ),
342 )
343 else:
344 result += meth( # type: ignore
345 attrname, obj, self, anon_map, bindparams
346 )
347 return result
348
349 def _generate_cache_key(self) -> Optional[CacheKey]:
350 """return a cache key.
351
352 The cache key is a tuple which can contain any series of
353 objects that are hashable and also identifies
354 this object uniquely within the presence of a larger SQL expression
355 or statement, for the purposes of caching the resulting query.
356
357 The cache key should be based on the SQL compiled structure that would
358 ultimately be produced. That is, two structures that are composed in
359 exactly the same way should produce the same cache key; any difference
360 in the structures that would affect the SQL string or the type handlers
361 should result in a different cache key.
362
363 The cache key returned by this method is an instance of
364 :class:`.CacheKey`, which consists of a tuple representing the
365 cache key, as well as a list of :class:`.BindParameter` objects
366 which are extracted from the expression. While two expressions
367 that produce identical cache key tuples will themselves generate
368 identical SQL strings, the list of :class:`.BindParameter` objects
369 indicates the bound values which may have different values in
370 each one; these bound parameters must be consulted in order to
371 execute the statement with the correct parameters.
372
373 a :class:`_expression.ClauseElement` structure that does not implement
374 a :meth:`._gen_cache_key` method and does not implement a
375 :attr:`.traverse_internals` attribute will not be cacheable; when
376 such an element is embedded into a larger structure, this method
377 will return None, indicating no cache key is available.
378
379 """
380
381 bindparams: List[BindParameter[Any]] = []
382
383 _anon_map = anon_map()
384 key = self._gen_cache_key(_anon_map, bindparams)
385 if NO_CACHE in _anon_map:
386 return None
387 else:
388 assert key is not None
389 return CacheKey(
390 key,
391 bindparams,
392 _anon_map.get(CacheConst.PARAMS), # type: ignore[arg-type]
393 )
394
395
396class HasCacheKeyTraverse(HasTraverseInternals, HasCacheKey):
397 pass
398
399
400class MemoizedHasCacheKey(HasCacheKey, HasMemoized):
401 __slots__ = ()
402
403 @HasMemoized.memoized_instancemethod
404 def _generate_cache_key(self) -> Optional[CacheKey]:
405 return HasCacheKey._generate_cache_key(self)
406
407
408class SlotsMemoizedHasCacheKey(HasCacheKey, util.MemoizedSlots):
409 __slots__ = ()
410
411 def _memoized_method__generate_cache_key(self) -> Optional[CacheKey]:
412 return HasCacheKey._generate_cache_key(self)
413
414
415class CacheKey(NamedTuple):
416 """The key used to identify a SQL statement construct in the
417 SQL compilation cache.
418
419 .. seealso::
420
421 :ref:`sql_caching`
422
423 """
424
425 key: Tuple[Any, ...]
426 bindparams: Sequence[BindParameter[Any]]
427 params: _CoreSingleExecuteParams | None
428
429 # can't set __hash__ attribute because it interferes
430 # with namedtuple
431 # can't use "if not TYPE_CHECKING" because mypy rejects it
432 # inside of a NamedTuple
433 def __hash__(self) -> Optional[int]: # type: ignore
434 """CacheKey itself is not hashable - hash the .key portion"""
435 return None
436
437 def to_offline_string(
438 self,
439 statement_cache: MutableMapping[Any, str],
440 statement: ClauseElement,
441 parameters: _CoreSingleExecuteParams,
442 ) -> str:
443 """Generate an "offline string" form of this :class:`.CacheKey`
444
445 The "offline string" is basically the string SQL for the
446 statement plus a repr of the bound parameter values in series.
447 Whereas the :class:`.CacheKey` object is dependent on in-memory
448 identities in order to work as a cache key, the "offline" version
449 is suitable for a cache that will work for other processes as well.
450
451 The given ``statement_cache`` is a dictionary-like object where the
452 string form of the statement itself will be cached. This dictionary
453 should be in a longer lived scope in order to reduce the time spent
454 stringifying statements.
455
456
457 """
458 if self.key not in statement_cache:
459 statement_cache[self.key] = sql_str = str(statement)
460 else:
461 sql_str = statement_cache[self.key]
462
463 if not self.bindparams:
464 param_tuple = tuple(parameters[key] for key in sorted(parameters))
465 else:
466 param_tuple = tuple(
467 parameters.get(bindparam.key, bindparam.value)
468 for bindparam in self.bindparams
469 )
470
471 return repr((sql_str, param_tuple))
472
473 def __eq__(self, other: Any) -> bool:
474 return other is not None and bool(self.key == other.key)
475
476 def __ne__(self, other: Any) -> bool:
477 return other is None or not (self.key == other.key)
478
479 @classmethod
480 def _diff_tuples(cls, left: CacheKey, right: CacheKey) -> str:
481 ck1 = CacheKey(left, [], None)
482 ck2 = CacheKey(right, [], None)
483 return ck1._diff(ck2)
484
485 def _whats_different(self, other: CacheKey) -> Iterator[str]:
486 k1 = self.key
487 k2 = other.key
488
489 stack: List[int] = []
490 pickup_index = 0
491 while True:
492 s1, s2 = k1, k2
493 for idx in stack:
494 s1 = s1[idx]
495 s2 = s2[idx]
496
497 for idx, (e1, e2) in enumerate(zip_longest(s1, s2)):
498 if idx < pickup_index:
499 continue
500 if e1 != e2:
501 if isinstance(e1, tuple) and isinstance(e2, tuple):
502 stack.append(idx)
503 break
504 else:
505 yield "key%s[%d]: %s != %s" % (
506 "".join("[%d]" % id_ for id_ in stack),
507 idx,
508 e1,
509 e2,
510 )
511 else:
512 stack.pop(-1)
513 break
514
515 def _diff(self, other: CacheKey) -> str:
516 return ", ".join(self._whats_different(other))
517
518 def __str__(self) -> str:
519 stack: List[Union[Tuple[Any, ...], HasCacheKey]] = [self.key]
520
521 output = []
522 sentinel = object()
523 indent = -1
524 while stack:
525 elem = stack.pop(0)
526 if elem is sentinel:
527 output.append((" " * (indent * 2)) + "),")
528 indent -= 1
529 elif isinstance(elem, tuple):
530 if not elem:
531 output.append((" " * ((indent + 1) * 2)) + "()")
532 else:
533 indent += 1
534 stack = list(elem) + [sentinel] + stack
535 output.append((" " * (indent * 2)) + "(")
536 else:
537 if isinstance(elem, HasCacheKey):
538 repr_ = "<%s object at %s>" % (
539 type(elem).__name__,
540 hex(id(elem)),
541 )
542 else:
543 repr_ = repr(elem)
544 output.append((" " * (indent * 2)) + " " + repr_ + ", ")
545
546 return "CacheKey(key=%s)" % ("\n".join(output),)
547
548 def _generate_param_dict(self) -> Dict[str, Any]:
549 """used for testing"""
550
551 _anon_map = prefix_anon_map()
552 return {b.key % _anon_map: b.effective_value for b in self.bindparams}
553
554 @util.preload_module("sqlalchemy.sql.elements")
555 def _apply_params_to_element(
556 self, original_cache_key: CacheKey, target_element: ColumnElement[Any]
557 ) -> ColumnElement[Any]:
558 if target_element._is_immutable or original_cache_key is self:
559 return target_element
560
561 elements = util.preloaded.sql_elements
562 return elements._OverrideBinds(
563 target_element, self.bindparams, original_cache_key.bindparams
564 )
565
566
567def _ad_hoc_cache_key_from_args(
568 tokens: Tuple[Any, ...],
569 traverse_args: Iterable[Tuple[str, InternalTraversal]],
570 args: Iterable[Any],
571) -> Tuple[Any, ...]:
572 """a quick cache key generator used by reflection.flexi_cache."""
573 bindparams: List[BindParameter[Any]] = []
574
575 _anon_map = anon_map()
576
577 tup = tokens
578
579 for (attrname, sym), arg in zip(traverse_args, args):
580 key = sym.name
581 visit_key = key.replace("dp_", "visit_")
582
583 if arg is None:
584 tup += (attrname, None)
585 continue
586
587 meth = getattr(_cache_key_traversal_visitor, visit_key)
588 if meth is CACHE_IN_PLACE:
589 tup += (attrname, arg)
590 elif meth in (
591 CALL_GEN_CACHE_KEY,
592 STATIC_CACHE_KEY,
593 ANON_NAME,
594 PROPAGATE_ATTRS,
595 ):
596 raise NotImplementedError(
597 f"Haven't implemented symbol {meth} for ad-hoc key from args"
598 )
599 else:
600 tup += meth(attrname, arg, None, _anon_map, bindparams)
601 return tup
602
603
604class _CacheKeyTraversal(HasTraversalDispatch):
605 # very common elements are inlined into the main _get_cache_key() method
606 # to produce a dramatic savings in Python function call overhead
607
608 visit_has_cache_key = visit_clauseelement = CALL_GEN_CACHE_KEY
609 visit_clauseelement_list = InternalTraversal.dp_clauseelement_list
610 visit_annotations_key = InternalTraversal.dp_annotations_key
611 visit_clauseelement_tuple = InternalTraversal.dp_clauseelement_tuple
612 visit_memoized_select_entities = (
613 InternalTraversal.dp_memoized_select_entities
614 )
615
616 visit_string = visit_boolean = visit_operator = visit_plain_obj = (
617 CACHE_IN_PLACE
618 )
619 visit_statement_hint_list = CACHE_IN_PLACE
620 visit_type = STATIC_CACHE_KEY
621 visit_anon_name = ANON_NAME
622
623 visit_propagate_attrs = PROPAGATE_ATTRS
624
625 def visit_compile_state_funcs(
626 self,
627 attrname: str,
628 obj: Any,
629 parent: Any,
630 anon_map: anon_map,
631 bindparams: List[BindParameter[Any]],
632 ) -> Tuple[Any, ...]:
633 return tuple((fn.__code__, c_key) for fn, c_key in obj)
634
635 def visit_inspectable(
636 self,
637 attrname: str,
638 obj: Any,
639 parent: Any,
640 anon_map: anon_map,
641 bindparams: List[BindParameter[Any]],
642 ) -> Tuple[Any, ...]:
643 return (attrname, inspect(obj)._gen_cache_key(anon_map, bindparams))
644
645 def visit_string_list(
646 self,
647 attrname: str,
648 obj: Any,
649 parent: Any,
650 anon_map: anon_map,
651 bindparams: List[BindParameter[Any]],
652 ) -> Tuple[Any, ...]:
653 return tuple(obj)
654
655 def visit_multi(
656 self,
657 attrname: str,
658 obj: Any,
659 parent: Any,
660 anon_map: anon_map,
661 bindparams: List[BindParameter[Any]],
662 ) -> Tuple[Any, ...]:
663 return (
664 attrname,
665 (
666 obj._gen_cache_key(anon_map, bindparams)
667 if isinstance(obj, HasCacheKey)
668 else obj
669 ),
670 )
671
672 def visit_multi_list(
673 self,
674 attrname: str,
675 obj: Any,
676 parent: Any,
677 anon_map: anon_map,
678 bindparams: List[BindParameter[Any]],
679 ) -> Tuple[Any, ...]:
680 return (
681 attrname,
682 tuple(
683 (
684 elem._gen_cache_key(anon_map, bindparams)
685 if isinstance(elem, HasCacheKey)
686 else elem
687 )
688 for elem in obj
689 ),
690 )
691
692 def visit_has_cache_key_tuples(
693 self,
694 attrname: str,
695 obj: Any,
696 parent: Any,
697 anon_map: anon_map,
698 bindparams: List[BindParameter[Any]],
699 ) -> Tuple[Any, ...]:
700 if not obj:
701 return ()
702 return (
703 attrname,
704 tuple(
705 tuple(
706 elem._gen_cache_key(anon_map, bindparams)
707 for elem in tup_elem
708 )
709 for tup_elem in obj
710 ),
711 )
712
713 def visit_has_cache_key_list(
714 self,
715 attrname: str,
716 obj: Any,
717 parent: Any,
718 anon_map: anon_map,
719 bindparams: List[BindParameter[Any]],
720 ) -> Tuple[Any, ...]:
721 if not obj:
722 return ()
723 return (
724 attrname,
725 tuple(elem._gen_cache_key(anon_map, bindparams) for elem in obj),
726 )
727
728 def visit_executable_options(
729 self,
730 attrname: str,
731 obj: Any,
732 parent: Any,
733 anon_map: anon_map,
734 bindparams: List[BindParameter[Any]],
735 ) -> Tuple[Any, ...]:
736 if not obj:
737 return ()
738 return (
739 attrname,
740 tuple(
741 elem._gen_cache_key(anon_map, bindparams)
742 for elem in obj
743 if elem._is_has_cache_key
744 ),
745 )
746
747 def visit_inspectable_list(
748 self,
749 attrname: str,
750 obj: Any,
751 parent: Any,
752 anon_map: anon_map,
753 bindparams: List[BindParameter[Any]],
754 ) -> Tuple[Any, ...]:
755 return self.visit_has_cache_key_list(
756 attrname, [inspect(o) for o in obj], parent, anon_map, bindparams
757 )
758
759 def visit_clauseelement_tuples(
760 self,
761 attrname: str,
762 obj: Any,
763 parent: Any,
764 anon_map: anon_map,
765 bindparams: List[BindParameter[Any]],
766 ) -> Tuple[Any, ...]:
767 return self.visit_has_cache_key_tuples(
768 attrname, obj, parent, anon_map, bindparams
769 )
770
771 def visit_fromclause_ordered_set(
772 self,
773 attrname: str,
774 obj: Any,
775 parent: Any,
776 anon_map: anon_map,
777 bindparams: List[BindParameter[Any]],
778 ) -> Tuple[Any, ...]:
779 if not obj:
780 return ()
781 return (
782 attrname,
783 tuple([elem._gen_cache_key(anon_map, bindparams) for elem in obj]),
784 )
785
786 def visit_clauseelement_unordered_set(
787 self,
788 attrname: str,
789 obj: Any,
790 parent: Any,
791 anon_map: anon_map,
792 bindparams: List[BindParameter[Any]],
793 ) -> Tuple[Any, ...]:
794 if not obj:
795 return ()
796 cache_keys = [
797 elem._gen_cache_key(anon_map, bindparams) for elem in obj
798 ]
799 return (
800 attrname,
801 tuple(
802 sorted(cache_keys)
803 ), # cache keys all start with (id_, class)
804 )
805
806 def visit_named_ddl_element(
807 self,
808 attrname: str,
809 obj: Any,
810 parent: Any,
811 anon_map: anon_map,
812 bindparams: List[BindParameter[Any]],
813 ) -> Tuple[Any, ...]:
814 return (attrname, obj.name)
815
816 def visit_prefix_sequence(
817 self,
818 attrname: str,
819 obj: Any,
820 parent: Any,
821 anon_map: anon_map,
822 bindparams: List[BindParameter[Any]],
823 ) -> Tuple[Any, ...]:
824 if not obj:
825 return ()
826
827 return (
828 attrname,
829 tuple(
830 [
831 (clause._gen_cache_key(anon_map, bindparams), strval)
832 for clause, strval in obj
833 ]
834 ),
835 )
836
837 def visit_setup_join_tuple(
838 self,
839 attrname: str,
840 obj: Any,
841 parent: Any,
842 anon_map: anon_map,
843 bindparams: List[BindParameter[Any]],
844 ) -> Tuple[Any, ...]:
845 return tuple(
846 (
847 target._gen_cache_key(anon_map, bindparams),
848 (
849 onclause._gen_cache_key(anon_map, bindparams)
850 if onclause is not None
851 else None
852 ),
853 (
854 from_._gen_cache_key(anon_map, bindparams)
855 if from_ is not None
856 else None
857 ),
858 tuple([(key, flags[key]) for key in sorted(flags)]),
859 )
860 for (target, onclause, from_, flags) in obj
861 )
862
863 def visit_table_hint_list(
864 self,
865 attrname: str,
866 obj: Any,
867 parent: Any,
868 anon_map: anon_map,
869 bindparams: List[BindParameter[Any]],
870 ) -> Tuple[Any, ...]:
871 if not obj:
872 return ()
873
874 return (
875 attrname,
876 tuple(
877 [
878 (
879 clause._gen_cache_key(anon_map, bindparams),
880 dialect_name,
881 text,
882 )
883 for (clause, dialect_name), text in obj.items()
884 ]
885 ),
886 )
887
888 def visit_plain_dict(
889 self,
890 attrname: str,
891 obj: Any,
892 parent: Any,
893 anon_map: anon_map,
894 bindparams: List[BindParameter[Any]],
895 ) -> Tuple[Any, ...]:
896 return (attrname, tuple([(key, obj[key]) for key in sorted(obj)]))
897
898 def visit_dialect_options(
899 self,
900 attrname: str,
901 obj: Any,
902 parent: Any,
903 anon_map: anon_map,
904 bindparams: List[BindParameter[Any]],
905 ) -> Tuple[Any, ...]:
906 return (
907 attrname,
908 tuple(
909 (
910 dialect_name,
911 tuple(
912 [
913 (key, obj[dialect_name][key])
914 for key in sorted(obj[dialect_name])
915 ]
916 ),
917 )
918 for dialect_name in sorted(obj)
919 ),
920 )
921
922 def visit_string_clauseelement_dict(
923 self,
924 attrname: str,
925 obj: Any,
926 parent: Any,
927 anon_map: anon_map,
928 bindparams: List[BindParameter[Any]],
929 ) -> Tuple[Any, ...]:
930 return (
931 attrname,
932 tuple(
933 (key, obj[key]._gen_cache_key(anon_map, bindparams))
934 for key in sorted(obj)
935 ),
936 )
937
938 def visit_string_multi_dict(
939 self,
940 attrname: str,
941 obj: Any,
942 parent: Any,
943 anon_map: anon_map,
944 bindparams: List[BindParameter[Any]],
945 ) -> Tuple[Any, ...]:
946 return (
947 attrname,
948 tuple(
949 (
950 key,
951 (
952 value._gen_cache_key(anon_map, bindparams)
953 if isinstance(value, HasCacheKey)
954 else value
955 ),
956 )
957 for key, value in [(key, obj[key]) for key in sorted(obj)]
958 ),
959 )
960
961 def visit_fromclause_canonical_column_collection(
962 self,
963 attrname: str,
964 obj: Any,
965 parent: Any,
966 anon_map: anon_map,
967 bindparams: List[BindParameter[Any]],
968 ) -> Tuple[Any, ...]:
969 # inlining into the internals of ColumnCollection
970 return (
971 attrname,
972 tuple(
973 col._gen_cache_key(anon_map, bindparams)
974 for k, col, _ in obj._collection
975 ),
976 )
977
978 def visit_unknown_structure(
979 self,
980 attrname: str,
981 obj: Any,
982 parent: Any,
983 anon_map: anon_map,
984 bindparams: List[BindParameter[Any]],
985 ) -> Tuple[Any, ...]:
986 anon_map[NO_CACHE] = True
987 return ()
988
989 def visit_dml_ordered_values(
990 self,
991 attrname: str,
992 obj: Any,
993 parent: Any,
994 anon_map: anon_map,
995 bindparams: List[BindParameter[Any]],
996 ) -> Tuple[Any, ...]:
997 return (
998 attrname,
999 tuple(
1000 (
1001 (
1002 key._gen_cache_key(anon_map, bindparams)
1003 if hasattr(key, "__clause_element__")
1004 else key
1005 ),
1006 value._gen_cache_key(anon_map, bindparams),
1007 )
1008 for key, value in obj
1009 ),
1010 )
1011
1012 def visit_dml_values(
1013 self,
1014 attrname: str,
1015 obj: Any,
1016 parent: Any,
1017 anon_map: anon_map,
1018 bindparams: List[BindParameter[Any]],
1019 ) -> Tuple[Any, ...]:
1020 # in py37 we can assume two dictionaries created in the same
1021 # insert ordering will retain that sorting
1022 return (
1023 attrname,
1024 tuple(
1025 (
1026 (
1027 k._gen_cache_key(anon_map, bindparams)
1028 if hasattr(k, "__clause_element__")
1029 else k
1030 ),
1031 obj[k]._gen_cache_key(anon_map, bindparams),
1032 )
1033 for k in obj
1034 ),
1035 )
1036
1037 def visit_dml_multi_values(
1038 self,
1039 attrname: str,
1040 obj: Any,
1041 parent: Any,
1042 anon_map: anon_map,
1043 bindparams: List[BindParameter[Any]],
1044 ) -> Tuple[Any, ...]:
1045 # multivalues are simply not cacheable right now
1046 anon_map[NO_CACHE] = True
1047 return ()
1048
1049 def visit_params(
1050 self,
1051 attrname: str,
1052 obj: Any,
1053 parent: Any,
1054 anon_map: anon_map,
1055 bindparams: List[BindParameter[Any]],
1056 ) -> Tuple[Any, ...]:
1057 if obj:
1058 if CacheConst.PARAMS in anon_map:
1059 to_set = anon_map[CacheConst.PARAMS] | obj
1060 else:
1061 to_set = obj
1062 anon_map[CacheConst.PARAMS] = to_set
1063 return ()
1064
1065
1066_cache_key_traversal_visitor = _CacheKeyTraversal()