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

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

322 statements  

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 Final 

17from typing import Iterable 

18from typing import Iterator 

19from typing import List 

20from typing import Literal 

21from typing import MutableMapping 

22from typing import NamedTuple 

23from typing import Optional 

24from typing import Protocol 

25from typing import Sequence 

26from typing import Tuple 

27from typing import Union 

28 

29from .visitors import anon_map 

30from .visitors import HasTraversalDispatch 

31from .visitors import HasTraverseInternals 

32from .visitors import InternalTraversal 

33from .visitors import prefix_anon_map 

34from .. import util 

35from ..inspection import inspect 

36from ..util import HasMemoized 

37 

38if typing.TYPE_CHECKING: 

39 from .elements import BindParameter 

40 from .elements import ClauseElement 

41 from .elements import ColumnElement 

42 from .visitors import _TraverseInternalsType 

43 from ..engine.interfaces import _CoreSingleExecuteParams 

44 

45 

46class _CacheKeyTraversalDispatchType(Protocol): 

47 def __call__( 

48 s, self: HasCacheKey, visitor: _CacheKeyTraversal 

49 ) -> _CacheKeyTraversalDispatchTypeReturn: ... 

50 

51 

52class CacheConst(enum.Enum): 

53 NO_CACHE = 0 

54 PARAMS = 1 

55 

56 

57NO_CACHE: Final = CacheConst.NO_CACHE 

58 

59 

60_CacheKeyTraversalType = Union[ 

61 "_TraverseInternalsType", Literal[CacheConst.NO_CACHE], Literal[None] 

62] 

63 

64 

65class CacheTraverseTarget(enum.Enum): 

66 CACHE_IN_PLACE = 0 

67 CALL_GEN_CACHE_KEY = 1 

68 STATIC_CACHE_KEY = 2 

69 PROPAGATE_ATTRS = 3 

70 ANON_NAME = 4 

71 

72 

73( 

74 CACHE_IN_PLACE, 

75 CALL_GEN_CACHE_KEY, 

76 STATIC_CACHE_KEY, 

77 PROPAGATE_ATTRS, 

78 ANON_NAME, 

79) = tuple(CacheTraverseTarget) 

80 

81_CacheKeyTraversalDispatchTypeReturn = Sequence[ 

82 Tuple[ 

83 str, 

84 Any, 

85 Union[ 

86 Callable[..., Tuple[Any, ...]], 

87 CacheTraverseTarget, 

88 InternalTraversal, 

89 ], 

90 ] 

91] 

92 

93 

94class HasCacheKey: 

95 """Mixin for objects which can produce a cache key. 

96 

97 This class is usually in a hierarchy that starts with the 

98 :class:`.HasTraverseInternals` base, but this is optional. Currently, 

99 the class should be able to work on its own without including 

100 :class:`.HasTraverseInternals`. 

101 

102 .. seealso:: 

103 

104 :class:`.CacheKey` 

105 

106 :ref:`sql_caching` 

107 

108 """ 

109 

110 __slots__ = () 

111 

112 _cache_key_traversal: _CacheKeyTraversalType = NO_CACHE 

113 

114 _is_has_cache_key = True 

115 

116 _hierarchy_supports_caching = True 

117 """private attribute which may be set to False to prevent the 

118 inherit_cache warning from being emitted for a hierarchy of subclasses. 

119 

120 Currently applies to the :class:`.ExecutableDDLElement` hierarchy which 

121 does not implement caching. 

122 

123 """ 

124 

125 inherit_cache: Optional[bool] = None 

126 """Indicate if this :class:`.HasCacheKey` instance should make use of the 

127 cache key generation scheme used by its immediate superclass. 

128 

129 The attribute defaults to ``None``, which indicates that a construct has 

130 not yet taken into account whether or not its appropriate for it to 

131 participate in caching; this is functionally equivalent to setting the 

132 value to ``False``, except that a warning is also emitted. 

133 

134 This flag can be set to ``True`` on a particular class, if the SQL that 

135 corresponds to the object does not change based on attributes which 

136 are local to this class, and not its superclass. 

137 

138 .. seealso:: 

139 

140 :ref:`compilerext_caching` - General guideslines for setting the 

141 :attr:`.HasCacheKey.inherit_cache` attribute for third-party or user 

142 defined SQL constructs. 

143 

144 """ 

145 

146 __slots__ = () 

147 

148 _generated_cache_key_traversal: Any 

149 

150 @classmethod 

151 def _generate_cache_attrs( 

152 cls, 

153 ) -> Union[_CacheKeyTraversalDispatchType, Literal[CacheConst.NO_CACHE]]: 

154 """generate cache key dispatcher for a new class. 

155 

156 This sets the _generated_cache_key_traversal attribute once called 

157 so should only be called once per class. 

158 

159 """ 

160 inherit_cache = cls.__dict__.get("inherit_cache", None) 

161 inherit = bool(inherit_cache) 

162 

163 if inherit: 

164 _cache_key_traversal = getattr(cls, "_cache_key_traversal", None) 

165 if _cache_key_traversal is None: 

166 try: 

167 assert issubclass(cls, HasTraverseInternals) 

168 _cache_key_traversal = cls._traverse_internals 

169 except AttributeError: 

170 cls._generated_cache_key_traversal = NO_CACHE 

171 return NO_CACHE 

172 

173 assert _cache_key_traversal is not NO_CACHE, ( 

174 f"class {cls} has _cache_key_traversal=NO_CACHE, " 

175 "which conflicts with inherit_cache=True" 

176 ) 

177 

178 # TODO: wouldn't we instead get this from our superclass? 

179 # also, our superclass may not have this yet, but in any case, 

180 # we'd generate for the superclass that has it. this is a little 

181 # more complicated, so for the moment this is a little less 

182 # efficient on startup but simpler. 

183 return _cache_key_traversal_visitor.generate_dispatch( 

184 cls, 

185 _cache_key_traversal, 

186 "_generated_cache_key_traversal", 

187 ) 

188 else: 

189 _cache_key_traversal = cls.__dict__.get( 

190 "_cache_key_traversal", None 

191 ) 

192 if _cache_key_traversal is None: 

193 _cache_key_traversal = cls.__dict__.get( 

194 "_traverse_internals", None 

195 ) 

196 if _cache_key_traversal is None: 

197 cls._generated_cache_key_traversal = NO_CACHE 

198 if ( 

199 inherit_cache is None 

200 and cls._hierarchy_supports_caching 

201 ): 

202 util.warn( 

203 "Class %s will not make use of SQL compilation " 

204 "caching as it does not set the 'inherit_cache' " 

205 "attribute to ``True``. This can have " 

206 "significant performance implications including " 

207 "some performance degradations in comparison to " 

208 "prior SQLAlchemy versions. Set this attribute " 

209 "to True if this object can make use of the cache " 

210 "key generated by the superclass. Alternatively, " 

211 "this attribute may be set to False which will " 

212 "disable this warning." % (cls.__name__), 

213 code="cprf", 

214 ) 

215 return NO_CACHE 

216 

217 return _cache_key_traversal_visitor.generate_dispatch( 

218 cls, 

219 _cache_key_traversal, 

220 "_generated_cache_key_traversal", 

221 ) 

222 

223 @util.preload_module("sqlalchemy.sql.elements") 

224 def _gen_cache_key( 

225 self, anon_map: anon_map, bindparams: List[BindParameter[Any]] 

226 ) -> Optional[Tuple[Any, ...]]: 

227 """return an optional cache key. 

228 

229 The cache key is a tuple which can contain any series of 

230 objects that are hashable and also identifies 

231 this object uniquely within the presence of a larger SQL expression 

232 or statement, for the purposes of caching the resulting query. 

233 

234 The cache key should be based on the SQL compiled structure that would 

235 ultimately be produced. That is, two structures that are composed in 

236 exactly the same way should produce the same cache key; any difference 

237 in the structures that would affect the SQL string or the type handlers 

238 should result in a different cache key. 

239 

240 If a structure cannot produce a useful cache key, the NO_CACHE 

241 symbol should be added to the anon_map and the method should 

242 return None. 

243 

244 """ 

245 

246 cls = self.__class__ 

247 

248 id_, found = anon_map.get_anon(self) 

249 if found: 

250 return (id_, cls) 

251 

252 dispatcher: Union[ 

253 Literal[CacheConst.NO_CACHE], 

254 _CacheKeyTraversalDispatchType, 

255 ] 

256 

257 try: 

258 dispatcher = cls.__dict__["_generated_cache_key_traversal"] 

259 except KeyError: 

260 # traversals.py -> _preconfigure_traversals() 

261 # may be used to run these ahead of time, but 

262 # is not enabled right now. 

263 # this block will generate any remaining dispatchers. 

264 dispatcher = cls._generate_cache_attrs() 

265 

266 if dispatcher is NO_CACHE: 

267 anon_map[NO_CACHE] = True 

268 return None 

269 

270 result: Tuple[Any, ...] = (id_, cls) 

271 

272 # inline of _cache_key_traversal_visitor.run_generated_dispatch() 

273 

274 for attrname, obj, meth in dispatcher( 

275 self, _cache_key_traversal_visitor 

276 ): 

277 if obj is not None: 

278 # TODO: see if C code can help here as Python lacks an 

279 # efficient switch construct 

280 

281 if meth is STATIC_CACHE_KEY: 

282 sck = obj._static_cache_key 

283 if sck is NO_CACHE: 

284 anon_map[NO_CACHE] = True 

285 return None 

286 result += (attrname, sck) 

287 elif meth is ANON_NAME: 

288 elements = util.preloaded.sql_elements 

289 if isinstance(obj, elements._anonymous_label): 

290 obj = obj.apply_map(anon_map) # type: ignore 

291 result += (attrname, obj) 

292 elif meth is CALL_GEN_CACHE_KEY: 

293 result += ( 

294 attrname, 

295 obj._gen_cache_key(anon_map, bindparams), 

296 ) 

297 

298 # remaining cache functions are against 

299 # Python tuples, dicts, lists, etc. so we can skip 

300 # if they are empty 

301 elif obj: 

302 if meth is CACHE_IN_PLACE: 

303 result += (attrname, obj) 

304 elif meth is PROPAGATE_ATTRS: 

305 result += ( 

306 attrname, 

307 obj["compile_state_plugin"], 

308 ( 

309 obj["plugin_subject"]._gen_cache_key( 

310 anon_map, bindparams 

311 ) 

312 if obj["plugin_subject"] 

313 else None 

314 ), 

315 ) 

316 elif meth is InternalTraversal.dp_annotations_key: 

317 # obj is here is the _annotations dict. Table uses 

318 # a memoized version of it. however in other cases, 

319 # we generate it given anon_map as we may be from a 

320 # Join, Aliased, etc. 

321 # see #8790 

322 

323 if self._gen_static_annotations_cache_key: # type: ignore # noqa: E501 

324 result += self._annotations_cache_key # type: ignore # noqa: E501 

325 else: 

326 result += self._gen_annotations_cache_key(anon_map) # type: ignore # noqa: E501 

327 

328 elif ( 

329 meth is InternalTraversal.dp_clauseelement_list 

330 or meth is InternalTraversal.dp_clauseelement_tuple 

331 or meth 

332 is InternalTraversal.dp_memoized_select_entities 

333 ): 

334 result += ( 

335 attrname, 

336 tuple( 

337 [ 

338 elem._gen_cache_key(anon_map, bindparams) 

339 for elem in obj 

340 ] 

341 ), 

342 ) 

343 else: 

344 result += meth( # type: ignore 

345 attrname, obj, self, anon_map, bindparams 

346 ) 

347 return result 

348 

349 def _generate_cache_key(self) -> Optional[CacheKey]: 

350 """return a cache key. 

351 

352 The cache key is a tuple which can contain any series of 

353 objects that are hashable and also identifies 

354 this object uniquely within the presence of a larger SQL expression 

355 or statement, for the purposes of caching the resulting query. 

356 

357 The cache key should be based on the SQL compiled structure that would 

358 ultimately be produced. That is, two structures that are composed in 

359 exactly the same way should produce the same cache key; any difference 

360 in the structures that would affect the SQL string or the type handlers 

361 should result in a different cache key. 

362 

363 The cache key returned by this method is an instance of 

364 :class:`.CacheKey`, which consists of a tuple representing the 

365 cache key, as well as a list of :class:`.BindParameter` objects 

366 which are extracted from the expression. While two expressions 

367 that produce identical cache key tuples will themselves generate 

368 identical SQL strings, the list of :class:`.BindParameter` objects 

369 indicates the bound values which may have different values in 

370 each one; these bound parameters must be consulted in order to 

371 execute the statement with the correct parameters. 

372 

373 a :class:`_expression.ClauseElement` structure that does not implement 

374 a :meth:`._gen_cache_key` method and does not implement a 

375 :attr:`.traverse_internals` attribute will not be cacheable; when 

376 such an element is embedded into a larger structure, this method 

377 will return None, indicating no cache key is available. 

378 

379 """ 

380 

381 bindparams: List[BindParameter[Any]] = [] 

382 

383 _anon_map = anon_map() 

384 key = self._gen_cache_key(_anon_map, bindparams) 

385 if NO_CACHE in _anon_map: 

386 return None 

387 else: 

388 assert key is not None 

389 return CacheKey( 

390 key, 

391 bindparams, 

392 _anon_map.get(CacheConst.PARAMS), # type: ignore[arg-type] 

393 ) 

394 

395 

396class HasCacheKeyTraverse(HasTraverseInternals, HasCacheKey): 

397 pass 

398 

399 

400class MemoizedHasCacheKey(HasCacheKey, HasMemoized): 

401 __slots__ = () 

402 

403 @HasMemoized.memoized_instancemethod 

404 def _generate_cache_key(self) -> Optional[CacheKey]: 

405 return HasCacheKey._generate_cache_key(self) 

406 

407 

408class SlotsMemoizedHasCacheKey(HasCacheKey, util.MemoizedSlots): 

409 __slots__ = () 

410 

411 def _memoized_method__generate_cache_key(self) -> Optional[CacheKey]: 

412 return HasCacheKey._generate_cache_key(self) 

413 

414 

415class CacheKey(NamedTuple): 

416 """The key used to identify a SQL statement construct in the 

417 SQL compilation cache. 

418 

419 .. seealso:: 

420 

421 :ref:`sql_caching` 

422 

423 """ 

424 

425 key: Tuple[Any, ...] 

426 bindparams: Sequence[BindParameter[Any]] 

427 params: _CoreSingleExecuteParams | None 

428 

429 # can't set __hash__ attribute because it interferes 

430 # with namedtuple 

431 # can't use "if not TYPE_CHECKING" because mypy rejects it 

432 # inside of a NamedTuple 

433 def __hash__(self) -> Optional[int]: # type: ignore 

434 """CacheKey itself is not hashable - hash the .key portion""" 

435 return None 

436 

437 def to_offline_string( 

438 self, 

439 statement_cache: MutableMapping[Any, str], 

440 statement: ClauseElement, 

441 parameters: _CoreSingleExecuteParams, 

442 ) -> str: 

443 """Generate an "offline string" form of this :class:`.CacheKey` 

444 

445 The "offline string" is basically the string SQL for the 

446 statement plus a repr of the bound parameter values in series. 

447 Whereas the :class:`.CacheKey` object is dependent on in-memory 

448 identities in order to work as a cache key, the "offline" version 

449 is suitable for a cache that will work for other processes as well. 

450 

451 The given ``statement_cache`` is a dictionary-like object where the 

452 string form of the statement itself will be cached. This dictionary 

453 should be in a longer lived scope in order to reduce the time spent 

454 stringifying statements. 

455 

456 

457 """ 

458 if self.key not in statement_cache: 

459 statement_cache[self.key] = sql_str = str(statement) 

460 else: 

461 sql_str = statement_cache[self.key] 

462 

463 if not self.bindparams: 

464 param_tuple = tuple(parameters[key] for key in sorted(parameters)) 

465 else: 

466 param_tuple = tuple( 

467 parameters.get(bindparam.key, bindparam.value) 

468 for bindparam in self.bindparams 

469 ) 

470 

471 return repr((sql_str, param_tuple)) 

472 

473 def __eq__(self, other: Any) -> bool: 

474 return other is not None and bool(self.key == other.key) 

475 

476 def __ne__(self, other: Any) -> bool: 

477 return other is None or not (self.key == other.key) 

478 

479 @classmethod 

480 def _diff_tuples(cls, left: CacheKey, right: CacheKey) -> str: 

481 ck1 = CacheKey(left, [], None) 

482 ck2 = CacheKey(right, [], None) 

483 return ck1._diff(ck2) 

484 

485 def _whats_different(self, other: CacheKey) -> Iterator[str]: 

486 k1 = self.key 

487 k2 = other.key 

488 

489 stack: List[int] = [] 

490 pickup_index = 0 

491 while True: 

492 s1, s2 = k1, k2 

493 for idx in stack: 

494 s1 = s1[idx] 

495 s2 = s2[idx] 

496 

497 for idx, (e1, e2) in enumerate(zip_longest(s1, s2)): 

498 if idx < pickup_index: 

499 continue 

500 if e1 != e2: 

501 if isinstance(e1, tuple) and isinstance(e2, tuple): 

502 stack.append(idx) 

503 break 

504 else: 

505 yield "key%s[%d]: %s != %s" % ( 

506 "".join("[%d]" % id_ for id_ in stack), 

507 idx, 

508 e1, 

509 e2, 

510 ) 

511 else: 

512 stack.pop(-1) 

513 break 

514 

515 def _diff(self, other: CacheKey) -> str: 

516 return ", ".join(self._whats_different(other)) 

517 

518 def __str__(self) -> str: 

519 stack: List[Union[Tuple[Any, ...], HasCacheKey]] = [self.key] 

520 

521 output = [] 

522 sentinel = object() 

523 indent = -1 

524 while stack: 

525 elem = stack.pop(0) 

526 if elem is sentinel: 

527 output.append((" " * (indent * 2)) + "),") 

528 indent -= 1 

529 elif isinstance(elem, tuple): 

530 if not elem: 

531 output.append((" " * ((indent + 1) * 2)) + "()") 

532 else: 

533 indent += 1 

534 stack = list(elem) + [sentinel] + stack 

535 output.append((" " * (indent * 2)) + "(") 

536 else: 

537 if isinstance(elem, HasCacheKey): 

538 repr_ = "<%s object at %s>" % ( 

539 type(elem).__name__, 

540 hex(id(elem)), 

541 ) 

542 else: 

543 repr_ = repr(elem) 

544 output.append((" " * (indent * 2)) + " " + repr_ + ", ") 

545 

546 return "CacheKey(key=%s)" % ("\n".join(output),) 

547 

548 def _generate_param_dict(self) -> Dict[str, Any]: 

549 """used for testing""" 

550 

551 _anon_map = prefix_anon_map() 

552 return {b.key % _anon_map: b.effective_value for b in self.bindparams} 

553 

554 @util.preload_module("sqlalchemy.sql.elements") 

555 def _apply_params_to_element( 

556 self, original_cache_key: CacheKey, target_element: ColumnElement[Any] 

557 ) -> ColumnElement[Any]: 

558 if target_element._is_immutable or original_cache_key is self: 

559 return target_element 

560 

561 elements = util.preloaded.sql_elements 

562 return elements._OverrideBinds( 

563 target_element, self.bindparams, original_cache_key.bindparams 

564 ) 

565 

566 

567def _ad_hoc_cache_key_from_args( 

568 tokens: Tuple[Any, ...], 

569 traverse_args: Iterable[Tuple[str, InternalTraversal]], 

570 args: Iterable[Any], 

571) -> Tuple[Any, ...]: 

572 """a quick cache key generator used by reflection.flexi_cache.""" 

573 bindparams: List[BindParameter[Any]] = [] 

574 

575 _anon_map = anon_map() 

576 

577 tup = tokens 

578 

579 for (attrname, sym), arg in zip(traverse_args, args): 

580 key = sym.name 

581 visit_key = key.replace("dp_", "visit_") 

582 

583 if arg is None: 

584 tup += (attrname, None) 

585 continue 

586 

587 meth = getattr(_cache_key_traversal_visitor, visit_key) 

588 if meth is CACHE_IN_PLACE: 

589 tup += (attrname, arg) 

590 elif meth in ( 

591 CALL_GEN_CACHE_KEY, 

592 STATIC_CACHE_KEY, 

593 ANON_NAME, 

594 PROPAGATE_ATTRS, 

595 ): 

596 raise NotImplementedError( 

597 f"Haven't implemented symbol {meth} for ad-hoc key from args" 

598 ) 

599 else: 

600 tup += meth(attrname, arg, None, _anon_map, bindparams) 

601 return tup 

602 

603 

604class _CacheKeyTraversal(HasTraversalDispatch): 

605 # very common elements are inlined into the main _get_cache_key() method 

606 # to produce a dramatic savings in Python function call overhead 

607 

608 visit_has_cache_key = visit_clauseelement = CALL_GEN_CACHE_KEY 

609 visit_clauseelement_list = InternalTraversal.dp_clauseelement_list 

610 visit_annotations_key = InternalTraversal.dp_annotations_key 

611 visit_clauseelement_tuple = InternalTraversal.dp_clauseelement_tuple 

612 visit_memoized_select_entities = ( 

613 InternalTraversal.dp_memoized_select_entities 

614 ) 

615 

616 visit_string = visit_boolean = visit_operator = visit_plain_obj = ( 

617 CACHE_IN_PLACE 

618 ) 

619 visit_statement_hint_list = CACHE_IN_PLACE 

620 visit_type = STATIC_CACHE_KEY 

621 visit_anon_name = ANON_NAME 

622 

623 visit_propagate_attrs = PROPAGATE_ATTRS 

624 

625 def visit_compile_state_funcs( 

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 tuple((fn.__code__, c_key) for fn, c_key in obj) 

634 

635 def visit_inspectable( 

636 self, 

637 attrname: str, 

638 obj: Any, 

639 parent: Any, 

640 anon_map: anon_map, 

641 bindparams: List[BindParameter[Any]], 

642 ) -> Tuple[Any, ...]: 

643 return (attrname, inspect(obj)._gen_cache_key(anon_map, bindparams)) 

644 

645 def visit_string_list( 

646 self, 

647 attrname: str, 

648 obj: Any, 

649 parent: Any, 

650 anon_map: anon_map, 

651 bindparams: List[BindParameter[Any]], 

652 ) -> Tuple[Any, ...]: 

653 return tuple(obj) 

654 

655 def visit_multi( 

656 self, 

657 attrname: str, 

658 obj: Any, 

659 parent: Any, 

660 anon_map: anon_map, 

661 bindparams: List[BindParameter[Any]], 

662 ) -> Tuple[Any, ...]: 

663 return ( 

664 attrname, 

665 ( 

666 obj._gen_cache_key(anon_map, bindparams) 

667 if isinstance(obj, HasCacheKey) 

668 else obj 

669 ), 

670 ) 

671 

672 def visit_multi_list( 

673 self, 

674 attrname: str, 

675 obj: Any, 

676 parent: Any, 

677 anon_map: anon_map, 

678 bindparams: List[BindParameter[Any]], 

679 ) -> Tuple[Any, ...]: 

680 return ( 

681 attrname, 

682 tuple( 

683 ( 

684 elem._gen_cache_key(anon_map, bindparams) 

685 if isinstance(elem, HasCacheKey) 

686 else elem 

687 ) 

688 for elem in obj 

689 ), 

690 ) 

691 

692 def visit_has_cache_key_tuples( 

693 self, 

694 attrname: str, 

695 obj: Any, 

696 parent: Any, 

697 anon_map: anon_map, 

698 bindparams: List[BindParameter[Any]], 

699 ) -> Tuple[Any, ...]: 

700 if not obj: 

701 return () 

702 return ( 

703 attrname, 

704 tuple( 

705 tuple( 

706 elem._gen_cache_key(anon_map, bindparams) 

707 for elem in tup_elem 

708 ) 

709 for tup_elem in obj 

710 ), 

711 ) 

712 

713 def visit_has_cache_key_list( 

714 self, 

715 attrname: str, 

716 obj: Any, 

717 parent: Any, 

718 anon_map: anon_map, 

719 bindparams: List[BindParameter[Any]], 

720 ) -> Tuple[Any, ...]: 

721 if not obj: 

722 return () 

723 return ( 

724 attrname, 

725 tuple(elem._gen_cache_key(anon_map, bindparams) for elem in obj), 

726 ) 

727 

728 def visit_executable_options( 

729 self, 

730 attrname: str, 

731 obj: Any, 

732 parent: Any, 

733 anon_map: anon_map, 

734 bindparams: List[BindParameter[Any]], 

735 ) -> Tuple[Any, ...]: 

736 if not obj: 

737 return () 

738 return ( 

739 attrname, 

740 tuple( 

741 elem._gen_cache_key(anon_map, bindparams) 

742 for elem in obj 

743 if elem._is_has_cache_key 

744 ), 

745 ) 

746 

747 def visit_inspectable_list( 

748 self, 

749 attrname: str, 

750 obj: Any, 

751 parent: Any, 

752 anon_map: anon_map, 

753 bindparams: List[BindParameter[Any]], 

754 ) -> Tuple[Any, ...]: 

755 return self.visit_has_cache_key_list( 

756 attrname, [inspect(o) for o in obj], parent, anon_map, bindparams 

757 ) 

758 

759 def visit_clauseelement_tuples( 

760 self, 

761 attrname: str, 

762 obj: Any, 

763 parent: Any, 

764 anon_map: anon_map, 

765 bindparams: List[BindParameter[Any]], 

766 ) -> Tuple[Any, ...]: 

767 return self.visit_has_cache_key_tuples( 

768 attrname, obj, parent, anon_map, bindparams 

769 ) 

770 

771 def visit_fromclause_ordered_set( 

772 self, 

773 attrname: str, 

774 obj: Any, 

775 parent: Any, 

776 anon_map: anon_map, 

777 bindparams: List[BindParameter[Any]], 

778 ) -> Tuple[Any, ...]: 

779 if not obj: 

780 return () 

781 return ( 

782 attrname, 

783 tuple([elem._gen_cache_key(anon_map, bindparams) for elem in obj]), 

784 ) 

785 

786 def visit_clauseelement_unordered_set( 

787 self, 

788 attrname: str, 

789 obj: Any, 

790 parent: Any, 

791 anon_map: anon_map, 

792 bindparams: List[BindParameter[Any]], 

793 ) -> Tuple[Any, ...]: 

794 if not obj: 

795 return () 

796 cache_keys = [ 

797 elem._gen_cache_key(anon_map, bindparams) for elem in obj 

798 ] 

799 return ( 

800 attrname, 

801 tuple( 

802 sorted(cache_keys) 

803 ), # cache keys all start with (id_, class) 

804 ) 

805 

806 def visit_named_ddl_element( 

807 self, 

808 attrname: str, 

809 obj: Any, 

810 parent: Any, 

811 anon_map: anon_map, 

812 bindparams: List[BindParameter[Any]], 

813 ) -> Tuple[Any, ...]: 

814 return (attrname, obj.name) 

815 

816 def visit_prefix_sequence( 

817 self, 

818 attrname: str, 

819 obj: Any, 

820 parent: Any, 

821 anon_map: anon_map, 

822 bindparams: List[BindParameter[Any]], 

823 ) -> Tuple[Any, ...]: 

824 if not obj: 

825 return () 

826 

827 return ( 

828 attrname, 

829 tuple( 

830 [ 

831 (clause._gen_cache_key(anon_map, bindparams), strval) 

832 for clause, strval in obj 

833 ] 

834 ), 

835 ) 

836 

837 def visit_setup_join_tuple( 

838 self, 

839 attrname: str, 

840 obj: Any, 

841 parent: Any, 

842 anon_map: anon_map, 

843 bindparams: List[BindParameter[Any]], 

844 ) -> Tuple[Any, ...]: 

845 return tuple( 

846 ( 

847 target._gen_cache_key(anon_map, bindparams), 

848 ( 

849 onclause._gen_cache_key(anon_map, bindparams) 

850 if onclause is not None 

851 else None 

852 ), 

853 ( 

854 from_._gen_cache_key(anon_map, bindparams) 

855 if from_ is not None 

856 else None 

857 ), 

858 tuple([(key, flags[key]) for key in sorted(flags)]), 

859 ) 

860 for (target, onclause, from_, flags) in obj 

861 ) 

862 

863 def visit_table_hint_list( 

864 self, 

865 attrname: str, 

866 obj: Any, 

867 parent: Any, 

868 anon_map: anon_map, 

869 bindparams: List[BindParameter[Any]], 

870 ) -> Tuple[Any, ...]: 

871 if not obj: 

872 return () 

873 

874 return ( 

875 attrname, 

876 tuple( 

877 [ 

878 ( 

879 clause._gen_cache_key(anon_map, bindparams), 

880 dialect_name, 

881 text, 

882 ) 

883 for (clause, dialect_name), text in obj.items() 

884 ] 

885 ), 

886 ) 

887 

888 def visit_plain_dict( 

889 self, 

890 attrname: str, 

891 obj: Any, 

892 parent: Any, 

893 anon_map: anon_map, 

894 bindparams: List[BindParameter[Any]], 

895 ) -> Tuple[Any, ...]: 

896 return (attrname, tuple([(key, obj[key]) for key in sorted(obj)])) 

897 

898 def visit_dialect_options( 

899 self, 

900 attrname: str, 

901 obj: Any, 

902 parent: Any, 

903 anon_map: anon_map, 

904 bindparams: List[BindParameter[Any]], 

905 ) -> Tuple[Any, ...]: 

906 return ( 

907 attrname, 

908 tuple( 

909 ( 

910 dialect_name, 

911 tuple( 

912 [ 

913 (key, obj[dialect_name][key]) 

914 for key in sorted(obj[dialect_name]) 

915 ] 

916 ), 

917 ) 

918 for dialect_name in sorted(obj) 

919 ), 

920 ) 

921 

922 def visit_string_clauseelement_dict( 

923 self, 

924 attrname: str, 

925 obj: Any, 

926 parent: Any, 

927 anon_map: anon_map, 

928 bindparams: List[BindParameter[Any]], 

929 ) -> Tuple[Any, ...]: 

930 return ( 

931 attrname, 

932 tuple( 

933 (key, obj[key]._gen_cache_key(anon_map, bindparams)) 

934 for key in sorted(obj) 

935 ), 

936 ) 

937 

938 def visit_string_multi_dict( 

939 self, 

940 attrname: str, 

941 obj: Any, 

942 parent: Any, 

943 anon_map: anon_map, 

944 bindparams: List[BindParameter[Any]], 

945 ) -> Tuple[Any, ...]: 

946 return ( 

947 attrname, 

948 tuple( 

949 ( 

950 key, 

951 ( 

952 value._gen_cache_key(anon_map, bindparams) 

953 if isinstance(value, HasCacheKey) 

954 else value 

955 ), 

956 ) 

957 for key, value in [(key, obj[key]) for key in sorted(obj)] 

958 ), 

959 ) 

960 

961 def visit_fromclause_canonical_column_collection( 

962 self, 

963 attrname: str, 

964 obj: Any, 

965 parent: Any, 

966 anon_map: anon_map, 

967 bindparams: List[BindParameter[Any]], 

968 ) -> Tuple[Any, ...]: 

969 # inlining into the internals of ColumnCollection 

970 return ( 

971 attrname, 

972 tuple( 

973 col._gen_cache_key(anon_map, bindparams) 

974 for k, col, _ in obj._collection 

975 ), 

976 ) 

977 

978 def visit_unknown_structure( 

979 self, 

980 attrname: str, 

981 obj: Any, 

982 parent: Any, 

983 anon_map: anon_map, 

984 bindparams: List[BindParameter[Any]], 

985 ) -> Tuple[Any, ...]: 

986 anon_map[NO_CACHE] = True 

987 return () 

988 

989 def visit_dml_ordered_values( 

990 self, 

991 attrname: str, 

992 obj: Any, 

993 parent: Any, 

994 anon_map: anon_map, 

995 bindparams: List[BindParameter[Any]], 

996 ) -> Tuple[Any, ...]: 

997 return ( 

998 attrname, 

999 tuple( 

1000 ( 

1001 ( 

1002 key._gen_cache_key(anon_map, bindparams) 

1003 if hasattr(key, "__clause_element__") 

1004 else key 

1005 ), 

1006 value._gen_cache_key(anon_map, bindparams), 

1007 ) 

1008 for key, value in obj 

1009 ), 

1010 ) 

1011 

1012 def visit_dml_values( 

1013 self, 

1014 attrname: str, 

1015 obj: Any, 

1016 parent: Any, 

1017 anon_map: anon_map, 

1018 bindparams: List[BindParameter[Any]], 

1019 ) -> Tuple[Any, ...]: 

1020 # in py37 we can assume two dictionaries created in the same 

1021 # insert ordering will retain that sorting 

1022 return ( 

1023 attrname, 

1024 tuple( 

1025 ( 

1026 ( 

1027 k._gen_cache_key(anon_map, bindparams) 

1028 if hasattr(k, "__clause_element__") 

1029 else k 

1030 ), 

1031 obj[k]._gen_cache_key(anon_map, bindparams), 

1032 ) 

1033 for k in obj 

1034 ), 

1035 ) 

1036 

1037 def visit_dml_multi_values( 

1038 self, 

1039 attrname: str, 

1040 obj: Any, 

1041 parent: Any, 

1042 anon_map: anon_map, 

1043 bindparams: List[BindParameter[Any]], 

1044 ) -> Tuple[Any, ...]: 

1045 # multivalues are simply not cacheable right now 

1046 anon_map[NO_CACHE] = True 

1047 return () 

1048 

1049 def visit_params( 

1050 self, 

1051 attrname: str, 

1052 obj: Any, 

1053 parent: Any, 

1054 anon_map: anon_map, 

1055 bindparams: List[BindParameter[Any]], 

1056 ) -> Tuple[Any, ...]: 

1057 if obj: 

1058 if CacheConst.PARAMS in anon_map: 

1059 to_set = anon_map[CacheConst.PARAMS] | obj 

1060 else: 

1061 to_set = obj 

1062 anon_map[CacheConst.PARAMS] = to_set 

1063 return () 

1064 

1065 

1066_cache_key_traversal_visitor = _CacheKeyTraversal()