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 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 Union 
    27 
    28from .visitors import anon_map 
    29from .visitors import HasTraversalDispatch 
    30from .visitors import HasTraverseInternals 
    31from .visitors import InternalTraversal 
    32from .visitors import prefix_anon_map 
    33from .. import util 
    34from ..inspection import inspect 
    35from ..util import HasMemoized 
    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 other is not None and bool(self.key == other.key) 
    482 
    483    def __ne__(self, other: Any) -> bool: 
    484        return other is None or 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                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_compile_state_funcs( 
    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()