Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/sqlalchemy/sql/cache_key.py: 49%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

317 statements  

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()