cache_key.py 33 KB

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