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