path_registry.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. # orm/path_registry.py
  2. # Copyright (C) 2005-2022 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. """Path tracking utilities, representing mapper graph traversals.
  8. """
  9. from itertools import chain
  10. import logging
  11. from . import base as orm_base
  12. from .. import exc
  13. from .. import inspection
  14. from .. import util
  15. from ..sql import visitors
  16. from ..sql.traversals import HasCacheKey
  17. log = logging.getLogger(__name__)
  18. def _unreduce_path(path):
  19. return PathRegistry.deserialize(path)
  20. _WILDCARD_TOKEN = "*"
  21. _DEFAULT_TOKEN = "_sa_default"
  22. class PathRegistry(HasCacheKey):
  23. """Represent query load paths and registry functions.
  24. Basically represents structures like:
  25. (<User mapper>, "orders", <Order mapper>, "items", <Item mapper>)
  26. These structures are generated by things like
  27. query options (joinedload(), subqueryload(), etc.) and are
  28. used to compose keys stored in the query._attributes dictionary
  29. for various options.
  30. They are then re-composed at query compile/result row time as
  31. the query is formed and as rows are fetched, where they again
  32. serve to compose keys to look up options in the context.attributes
  33. dictionary, which is copied from query._attributes.
  34. The path structure has a limited amount of caching, where each
  35. "root" ultimately pulls from a fixed registry associated with
  36. the first mapper, that also contains elements for each of its
  37. property keys. However paths longer than two elements, which
  38. are the exception rather than the rule, are generated on an
  39. as-needed basis.
  40. """
  41. __slots__ = ()
  42. is_token = False
  43. is_root = False
  44. _cache_key_traversal = [
  45. ("path", visitors.ExtendedInternalTraversal.dp_has_cache_key_list)
  46. ]
  47. def __eq__(self, other):
  48. try:
  49. return other is not None and self.path == other._path_for_compare
  50. except AttributeError:
  51. util.warn(
  52. "Comparison of PathRegistry to %r is not supported"
  53. % (type(other))
  54. )
  55. return False
  56. def __ne__(self, other):
  57. try:
  58. return other is None or self.path != other._path_for_compare
  59. except AttributeError:
  60. util.warn(
  61. "Comparison of PathRegistry to %r is not supported"
  62. % (type(other))
  63. )
  64. return True
  65. @property
  66. def _path_for_compare(self):
  67. return self.path
  68. def set(self, attributes, key, value):
  69. log.debug("set '%s' on path '%s' to '%s'", key, self, value)
  70. attributes[(key, self.natural_path)] = value
  71. def setdefault(self, attributes, key, value):
  72. log.debug("setdefault '%s' on path '%s' to '%s'", key, self, value)
  73. attributes.setdefault((key, self.natural_path), value)
  74. def get(self, attributes, key, value=None):
  75. key = (key, self.natural_path)
  76. if key in attributes:
  77. return attributes[key]
  78. else:
  79. return value
  80. def __len__(self):
  81. return len(self.path)
  82. def __hash__(self):
  83. return id(self)
  84. @property
  85. def length(self):
  86. return len(self.path)
  87. def pairs(self):
  88. path = self.path
  89. for i in range(0, len(path), 2):
  90. yield path[i], path[i + 1]
  91. def contains_mapper(self, mapper):
  92. for path_mapper in [self.path[i] for i in range(0, len(self.path), 2)]:
  93. if path_mapper.is_mapper and path_mapper.isa(mapper):
  94. return True
  95. else:
  96. return False
  97. def contains(self, attributes, key):
  98. return (key, self.path) in attributes
  99. def __reduce__(self):
  100. return _unreduce_path, (self.serialize(),)
  101. @classmethod
  102. def _serialize_path(cls, path):
  103. return list(
  104. zip(
  105. [
  106. m.class_ if (m.is_mapper or m.is_aliased_class) else str(m)
  107. for m in [path[i] for i in range(0, len(path), 2)]
  108. ],
  109. [
  110. path[i].key if (path[i].is_property) else str(path[i])
  111. for i in range(1, len(path), 2)
  112. ]
  113. + [None],
  114. )
  115. )
  116. @classmethod
  117. def _deserialize_path(cls, path):
  118. def _deserialize_mapper_token(mcls):
  119. return (
  120. # note: we likely dont want configure=True here however
  121. # this is maintained at the moment for backwards compatibility
  122. orm_base._inspect_mapped_class(mcls, configure=True)
  123. if mcls not in PathToken._intern
  124. else PathToken._intern[mcls]
  125. )
  126. def _deserialize_key_token(mcls, key):
  127. if key is None:
  128. return None
  129. elif key in PathToken._intern:
  130. return PathToken._intern[key]
  131. else:
  132. return orm_base._inspect_mapped_class(
  133. mcls, configure=True
  134. ).attrs[key]
  135. p = tuple(
  136. chain(
  137. *[
  138. (
  139. _deserialize_mapper_token(mcls),
  140. _deserialize_key_token(mcls, key),
  141. )
  142. for mcls, key in path
  143. ]
  144. )
  145. )
  146. if p and p[-1] is None:
  147. p = p[0:-1]
  148. return p
  149. @classmethod
  150. def serialize_context_dict(cls, dict_, tokens):
  151. return [
  152. ((key, cls._serialize_path(path)), value)
  153. for (key, path), value in [
  154. (k, v)
  155. for k, v in dict_.items()
  156. if isinstance(k, tuple) and k[0] in tokens
  157. ]
  158. ]
  159. @classmethod
  160. def deserialize_context_dict(cls, serialized):
  161. return util.OrderedDict(
  162. ((key, tuple(cls._deserialize_path(path))), value)
  163. for (key, path), value in serialized
  164. )
  165. def serialize(self):
  166. path = self.path
  167. return self._serialize_path(path)
  168. @classmethod
  169. def deserialize(cls, path):
  170. if path is None:
  171. return None
  172. p = cls._deserialize_path(path)
  173. return cls.coerce(p)
  174. @classmethod
  175. def per_mapper(cls, mapper):
  176. if mapper.is_mapper:
  177. return CachingEntityRegistry(cls.root, mapper)
  178. else:
  179. return SlotsEntityRegistry(cls.root, mapper)
  180. @classmethod
  181. def coerce(cls, raw):
  182. return util.reduce(lambda prev, next: prev[next], raw, cls.root)
  183. def token(self, token):
  184. if token.endswith(":" + _WILDCARD_TOKEN):
  185. return TokenRegistry(self, token)
  186. elif token.endswith(":" + _DEFAULT_TOKEN):
  187. return TokenRegistry(self.root, token)
  188. else:
  189. raise exc.ArgumentError("invalid token: %s" % token)
  190. def __add__(self, other):
  191. return util.reduce(lambda prev, next: prev[next], other.path, self)
  192. def __repr__(self):
  193. return "%s(%r)" % (self.__class__.__name__, self.path)
  194. class RootRegistry(PathRegistry):
  195. """Root registry, defers to mappers so that
  196. paths are maintained per-root-mapper.
  197. """
  198. inherit_cache = True
  199. path = natural_path = ()
  200. has_entity = False
  201. is_aliased_class = False
  202. is_root = True
  203. def __getitem__(self, entity):
  204. if entity in PathToken._intern:
  205. return PathToken._intern[entity]
  206. else:
  207. return entity._path_registry
  208. PathRegistry.root = RootRegistry()
  209. class PathToken(orm_base.InspectionAttr, HasCacheKey, str):
  210. """cacheable string token"""
  211. _intern = {}
  212. def _gen_cache_key(self, anon_map, bindparams):
  213. return (str(self),)
  214. @property
  215. def _path_for_compare(self):
  216. return None
  217. @classmethod
  218. def intern(cls, strvalue):
  219. if strvalue in cls._intern:
  220. return cls._intern[strvalue]
  221. else:
  222. cls._intern[strvalue] = result = PathToken(strvalue)
  223. return result
  224. class TokenRegistry(PathRegistry):
  225. __slots__ = ("token", "parent", "path", "natural_path")
  226. inherit_cache = True
  227. def __init__(self, parent, token):
  228. token = PathToken.intern(token)
  229. self.token = token
  230. self.parent = parent
  231. self.path = parent.path + (token,)
  232. self.natural_path = parent.natural_path + (token,)
  233. has_entity = False
  234. is_token = True
  235. def generate_for_superclasses(self):
  236. if not self.parent.is_aliased_class and not self.parent.is_root:
  237. for ent in self.parent.mapper.iterate_to_root():
  238. yield TokenRegistry(self.parent.parent[ent], self.token)
  239. elif (
  240. self.parent.is_aliased_class
  241. and self.parent.entity._is_with_polymorphic
  242. ):
  243. yield self
  244. for ent in self.parent.entity._with_polymorphic_entities:
  245. yield TokenRegistry(self.parent.parent[ent], self.token)
  246. else:
  247. yield self
  248. def __getitem__(self, entity):
  249. raise NotImplementedError()
  250. class PropRegistry(PathRegistry):
  251. is_unnatural = False
  252. inherit_cache = True
  253. def __init__(self, parent, prop):
  254. # restate this path in terms of the
  255. # given MapperProperty's parent.
  256. insp = inspection.inspect(parent[-1])
  257. natural_parent = parent
  258. if not insp.is_aliased_class or insp._use_mapper_path:
  259. parent = natural_parent = parent.parent[prop.parent]
  260. elif (
  261. insp.is_aliased_class
  262. and insp.with_polymorphic_mappers
  263. and prop.parent in insp.with_polymorphic_mappers
  264. ):
  265. subclass_entity = parent[-1]._entity_for_mapper(prop.parent)
  266. parent = parent.parent[subclass_entity]
  267. # when building a path where with_polymorphic() is in use,
  268. # special logic to determine the "natural path" when subclass
  269. # entities are used.
  270. #
  271. # here we are trying to distinguish between a path that starts
  272. # on a the with_polymorhpic entity vs. one that starts on a
  273. # normal entity that introduces a with_polymorphic() in the
  274. # middle using of_type():
  275. #
  276. # # as in test_polymorphic_rel->
  277. # # test_subqueryload_on_subclass_uses_path_correctly
  278. # wp = with_polymorphic(RegularEntity, "*")
  279. # sess.query(wp).options(someload(wp.SomeSubEntity.foos))
  280. #
  281. # vs
  282. #
  283. # # as in test_relationship->JoinedloadWPolyOfTypeContinued
  284. # wp = with_polymorphic(SomeFoo, "*")
  285. # sess.query(RegularEntity).options(
  286. # someload(RegularEntity.foos.of_type(wp))
  287. # .someload(wp.SubFoo.bar)
  288. # )
  289. #
  290. # in the former case, the Query as it generates a path that we
  291. # want to match will be in terms of the with_polymorphic at the
  292. # beginning. in the latter case, Query will generate simple
  293. # paths that don't know about this with_polymorphic, so we must
  294. # use a separate natural path.
  295. #
  296. #
  297. if parent.parent:
  298. natural_parent = parent.parent[subclass_entity.mapper]
  299. self.is_unnatural = True
  300. else:
  301. natural_parent = parent
  302. elif (
  303. natural_parent.parent
  304. and insp.is_aliased_class
  305. and prop.parent # this should always be the case here
  306. is not insp.mapper
  307. and insp.mapper.isa(prop.parent)
  308. ):
  309. natural_parent = parent.parent[prop.parent]
  310. self.prop = prop
  311. self.parent = parent
  312. self.path = parent.path + (prop,)
  313. self.natural_path = natural_parent.natural_path + (prop,)
  314. self._wildcard_path_loader_key = (
  315. "loader",
  316. parent.path + self.prop._wildcard_token,
  317. )
  318. self._default_path_loader_key = self.prop._default_path_loader_key
  319. self._loader_key = ("loader", self.natural_path)
  320. def __str__(self):
  321. return " -> ".join(str(elem) for elem in self.path)
  322. @util.memoized_property
  323. def has_entity(self):
  324. return self.prop._links_to_entity
  325. @util.memoized_property
  326. def entity(self):
  327. return self.prop.entity
  328. @property
  329. def mapper(self):
  330. return self.prop.mapper
  331. @property
  332. def entity_path(self):
  333. return self[self.entity]
  334. def __getitem__(self, entity):
  335. if isinstance(entity, (int, slice)):
  336. return self.path[entity]
  337. else:
  338. return SlotsEntityRegistry(self, entity)
  339. class AbstractEntityRegistry(PathRegistry):
  340. __slots__ = ()
  341. has_entity = True
  342. def __init__(self, parent, entity):
  343. self.key = entity
  344. self.parent = parent
  345. self.is_aliased_class = entity.is_aliased_class
  346. self.entity = entity
  347. self.path = parent.path + (entity,)
  348. # the "natural path" is the path that we get when Query is traversing
  349. # from the lead entities into the various relationships; it corresponds
  350. # to the structure of mappers and relationships. when we are given a
  351. # path that comes from loader options, as of 1.3 it can have ac-hoc
  352. # with_polymorphic() and other AliasedInsp objects inside of it, which
  353. # are usually not present in mappings. So here we track both the
  354. # "enhanced" path in self.path and the "natural" path that doesn't
  355. # include those objects so these two traversals can be matched up.
  356. # the test here for "(self.is_aliased_class or parent.is_unnatural)"
  357. # are to avoid the more expensive conditional logic that follows if we
  358. # know we don't have to do it. This conditional can just as well be
  359. # "if parent.path:", it just is more function calls.
  360. if parent.path and (self.is_aliased_class or parent.is_unnatural):
  361. # this is an infrequent code path used only for loader strategies
  362. # that also make use of of_type().
  363. if entity.mapper.isa(parent.natural_path[-1].entity):
  364. self.natural_path = parent.natural_path + (entity.mapper,)
  365. else:
  366. self.natural_path = parent.natural_path + (
  367. parent.natural_path[-1].entity,
  368. )
  369. # it seems to make sense that since these paths get mixed up
  370. # with statements that are cached or not, we should make
  371. # sure the natural path is cacheable across different occurrences
  372. # of equivalent AliasedClass objects. however, so far this
  373. # does not seem to be needed for whatever reason.
  374. # elif not parent.path and self.is_aliased_class:
  375. # self.natural_path = (self.entity._generate_cache_key()[0], )
  376. else:
  377. # self.natural_path = parent.natural_path + (entity, )
  378. self.natural_path = self.path
  379. @property
  380. def entity_path(self):
  381. return self
  382. @property
  383. def mapper(self):
  384. return inspection.inspect(self.entity).mapper
  385. def __bool__(self):
  386. return True
  387. __nonzero__ = __bool__
  388. def __getitem__(self, entity):
  389. if isinstance(entity, (int, slice)):
  390. return self.path[entity]
  391. elif entity in PathToken._intern:
  392. return TokenRegistry(self, PathToken._intern[entity])
  393. else:
  394. return PropRegistry(self, entity)
  395. class SlotsEntityRegistry(AbstractEntityRegistry):
  396. # for aliased class, return lightweight, no-cycles created
  397. # version
  398. inherit_cache = True
  399. __slots__ = (
  400. "key",
  401. "parent",
  402. "is_aliased_class",
  403. "entity",
  404. "path",
  405. "natural_path",
  406. )
  407. class CachingEntityRegistry(AbstractEntityRegistry, dict):
  408. # for long lived mapper, return dict based caching
  409. # version that creates reference cycles
  410. inherit_cache = True
  411. def __getitem__(self, entity):
  412. if isinstance(entity, (int, slice)):
  413. return self.path[entity]
  414. else:
  415. return dict.__getitem__(self, entity)
  416. def __missing__(self, key):
  417. self[key] = item = PropRegistry(self, key)
  418. return item