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