You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

483 lines
15KB

  1. # orm/path_registry.py
  2. # Copyright (C) 2005-2021 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: http://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 .base import class_mapper
  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
  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
  59. except AttributeError:
  60. util.warn(
  61. "Comparison of PathRegistry to %r is not supported"
  62. % (type(other))
  63. )
  64. return True
  65. def set(self, attributes, key, value):
  66. log.debug("set '%s' on path '%s' to '%s'", key, self, value)
  67. attributes[(key, self.natural_path)] = value
  68. def setdefault(self, attributes, key, value):
  69. log.debug("setdefault '%s' on path '%s' to '%s'", key, self, value)
  70. attributes.setdefault((key, self.natural_path), value)
  71. def get(self, attributes, key, value=None):
  72. key = (key, self.natural_path)
  73. if key in attributes:
  74. return attributes[key]
  75. else:
  76. return value
  77. def __len__(self):
  78. return len(self.path)
  79. def __hash__(self):
  80. return id(self)
  81. @property
  82. def length(self):
  83. return len(self.path)
  84. def pairs(self):
  85. path = self.path
  86. for i in range(0, len(path), 2):
  87. yield path[i], path[i + 1]
  88. def contains_mapper(self, mapper):
  89. for path_mapper in [self.path[i] for i in range(0, len(self.path), 2)]:
  90. if path_mapper.is_mapper and path_mapper.isa(mapper):
  91. return True
  92. else:
  93. return False
  94. def contains(self, attributes, key):
  95. return (key, self.path) in attributes
  96. def __reduce__(self):
  97. return _unreduce_path, (self.serialize(),)
  98. @classmethod
  99. def _serialize_path(cls, path):
  100. return list(
  101. zip(
  102. [m.class_ for m in [path[i] for i in range(0, len(path), 2)]],
  103. [path[i].key for i in range(1, len(path), 2)] + [None],
  104. )
  105. )
  106. @classmethod
  107. def _deserialize_path(cls, path):
  108. p = tuple(
  109. chain(
  110. *[
  111. (
  112. class_mapper(mcls),
  113. class_mapper(mcls).attrs[key]
  114. if key is not None
  115. else None,
  116. )
  117. for mcls, key in path
  118. ]
  119. )
  120. )
  121. if p and p[-1] is None:
  122. p = p[0:-1]
  123. return p
  124. @classmethod
  125. def serialize_context_dict(cls, dict_, tokens):
  126. return [
  127. ((key, cls._serialize_path(path)), value)
  128. for (key, path), value in [
  129. (k, v)
  130. for k, v in dict_.items()
  131. if isinstance(k, tuple) and k[0] in tokens
  132. ]
  133. ]
  134. @classmethod
  135. def deserialize_context_dict(cls, serialized):
  136. return util.OrderedDict(
  137. ((key, tuple(cls._deserialize_path(path))), value)
  138. for (key, path), value in serialized
  139. )
  140. def serialize(self):
  141. path = self.path
  142. return self._serialize_path(path)
  143. @classmethod
  144. def deserialize(cls, path):
  145. if path is None:
  146. return None
  147. p = cls._deserialize_path(path)
  148. return cls.coerce(p)
  149. @classmethod
  150. def per_mapper(cls, mapper):
  151. if mapper.is_mapper:
  152. return CachingEntityRegistry(cls.root, mapper)
  153. else:
  154. return SlotsEntityRegistry(cls.root, mapper)
  155. @classmethod
  156. def coerce(cls, raw):
  157. return util.reduce(lambda prev, next: prev[next], raw, cls.root)
  158. def token(self, token):
  159. if token.endswith(":" + _WILDCARD_TOKEN):
  160. return TokenRegistry(self, token)
  161. elif token.endswith(":" + _DEFAULT_TOKEN):
  162. return TokenRegistry(self.root, token)
  163. else:
  164. raise exc.ArgumentError("invalid token: %s" % token)
  165. def __add__(self, other):
  166. return util.reduce(lambda prev, next: prev[next], other.path, self)
  167. def __repr__(self):
  168. return "%s(%r)" % (self.__class__.__name__, self.path)
  169. class RootRegistry(PathRegistry):
  170. """Root registry, defers to mappers so that
  171. paths are maintained per-root-mapper.
  172. """
  173. inherit_cache = True
  174. path = natural_path = ()
  175. has_entity = False
  176. is_aliased_class = False
  177. is_root = True
  178. def __getitem__(self, entity):
  179. return entity._path_registry
  180. PathRegistry.root = RootRegistry()
  181. class PathToken(HasCacheKey, str):
  182. """cacheable string token"""
  183. _intern = {}
  184. def _gen_cache_key(self, anon_map, bindparams):
  185. return (str(self),)
  186. @classmethod
  187. def intern(cls, strvalue):
  188. if strvalue in cls._intern:
  189. return cls._intern[strvalue]
  190. else:
  191. cls._intern[strvalue] = result = PathToken(strvalue)
  192. return result
  193. class TokenRegistry(PathRegistry):
  194. __slots__ = ("token", "parent", "path", "natural_path")
  195. inherit_cache = True
  196. def __init__(self, parent, token):
  197. token = PathToken.intern(token)
  198. self.token = token
  199. self.parent = parent
  200. self.path = parent.path + (token,)
  201. self.natural_path = parent.natural_path + (token,)
  202. has_entity = False
  203. is_token = True
  204. def generate_for_superclasses(self):
  205. if not self.parent.is_aliased_class and not self.parent.is_root:
  206. for ent in self.parent.mapper.iterate_to_root():
  207. yield TokenRegistry(self.parent.parent[ent], self.token)
  208. elif (
  209. self.parent.is_aliased_class
  210. and self.parent.entity._is_with_polymorphic
  211. ):
  212. yield self
  213. for ent in self.parent.entity._with_polymorphic_entities:
  214. yield TokenRegistry(self.parent.parent[ent], self.token)
  215. else:
  216. yield self
  217. def __getitem__(self, entity):
  218. raise NotImplementedError()
  219. class PropRegistry(PathRegistry):
  220. is_unnatural = False
  221. inherit_cache = True
  222. def __init__(self, parent, prop):
  223. # restate this path in terms of the
  224. # given MapperProperty's parent.
  225. insp = inspection.inspect(parent[-1])
  226. natural_parent = parent
  227. if not insp.is_aliased_class or insp._use_mapper_path:
  228. parent = natural_parent = parent.parent[prop.parent]
  229. elif (
  230. insp.is_aliased_class
  231. and insp.with_polymorphic_mappers
  232. and prop.parent in insp.with_polymorphic_mappers
  233. ):
  234. subclass_entity = parent[-1]._entity_for_mapper(prop.parent)
  235. parent = parent.parent[subclass_entity]
  236. # when building a path where with_polymorphic() is in use,
  237. # special logic to determine the "natural path" when subclass
  238. # entities are used.
  239. #
  240. # here we are trying to distinguish between a path that starts
  241. # on a the with_polymorhpic entity vs. one that starts on a
  242. # normal entity that introduces a with_polymorphic() in the
  243. # middle using of_type():
  244. #
  245. # # as in test_polymorphic_rel->
  246. # # test_subqueryload_on_subclass_uses_path_correctly
  247. # wp = with_polymorphic(RegularEntity, "*")
  248. # sess.query(wp).options(someload(wp.SomeSubEntity.foos))
  249. #
  250. # vs
  251. #
  252. # # as in test_relationship->JoinedloadWPolyOfTypeContinued
  253. # wp = with_polymorphic(SomeFoo, "*")
  254. # sess.query(RegularEntity).options(
  255. # someload(RegularEntity.foos.of_type(wp))
  256. # .someload(wp.SubFoo.bar)
  257. # )
  258. #
  259. # in the former case, the Query as it generates a path that we
  260. # want to match will be in terms of the with_polymorphic at the
  261. # beginning. in the latter case, Query will generate simple
  262. # paths that don't know about this with_polymorphic, so we must
  263. # use a separate natural path.
  264. #
  265. #
  266. if parent.parent:
  267. natural_parent = parent.parent[subclass_entity.mapper]
  268. self.is_unnatural = True
  269. else:
  270. natural_parent = parent
  271. elif (
  272. natural_parent.parent
  273. and insp.is_aliased_class
  274. and prop.parent # this should always be the case here
  275. is not insp.mapper
  276. and insp.mapper.isa(prop.parent)
  277. ):
  278. natural_parent = parent.parent[prop.parent]
  279. self.prop = prop
  280. self.parent = parent
  281. self.path = parent.path + (prop,)
  282. self.natural_path = natural_parent.natural_path + (prop,)
  283. self._wildcard_path_loader_key = (
  284. "loader",
  285. parent.path + self.prop._wildcard_token,
  286. )
  287. self._default_path_loader_key = self.prop._default_path_loader_key
  288. self._loader_key = ("loader", self.natural_path)
  289. def __str__(self):
  290. return " -> ".join(str(elem) for elem in self.path)
  291. @util.memoized_property
  292. def has_entity(self):
  293. return hasattr(self.prop, "mapper")
  294. @util.memoized_property
  295. def entity(self):
  296. return self.prop.mapper
  297. @property
  298. def mapper(self):
  299. return self.entity
  300. @property
  301. def entity_path(self):
  302. return self[self.entity]
  303. def __getitem__(self, entity):
  304. if isinstance(entity, (int, slice)):
  305. return self.path[entity]
  306. else:
  307. return SlotsEntityRegistry(self, entity)
  308. class AbstractEntityRegistry(PathRegistry):
  309. __slots__ = ()
  310. has_entity = True
  311. def __init__(self, parent, entity):
  312. self.key = entity
  313. self.parent = parent
  314. self.is_aliased_class = entity.is_aliased_class
  315. self.entity = entity
  316. self.path = parent.path + (entity,)
  317. # the "natural path" is the path that we get when Query is traversing
  318. # from the lead entities into the various relationships; it corresponds
  319. # to the structure of mappers and relationships. when we are given a
  320. # path that comes from loader options, as of 1.3 it can have ac-hoc
  321. # with_polymorphic() and other AliasedInsp objects inside of it, which
  322. # are usually not present in mappings. So here we track both the
  323. # "enhanced" path in self.path and the "natural" path that doesn't
  324. # include those objects so these two traversals can be matched up.
  325. # the test here for "(self.is_aliased_class or parent.is_unnatural)"
  326. # are to avoid the more expensive conditional logic that follows if we
  327. # know we don't have to do it. This conditional can just as well be
  328. # "if parent.path:", it just is more function calls.
  329. if parent.path and (self.is_aliased_class or parent.is_unnatural):
  330. # this is an infrequent code path used only for loader strategies
  331. # that also make use of of_type().
  332. if entity.mapper.isa(parent.natural_path[-1].entity):
  333. self.natural_path = parent.natural_path + (entity.mapper,)
  334. else:
  335. self.natural_path = parent.natural_path + (
  336. parent.natural_path[-1].entity,
  337. )
  338. # it seems to make sense that since these paths get mixed up
  339. # with statements that are cached or not, we should make
  340. # sure the natural path is cachable across different occurrences
  341. # of equivalent AliasedClass objects. however, so far this
  342. # does not seem to be needed for whatever reason.
  343. # elif not parent.path and self.is_aliased_class:
  344. # self.natural_path = (self.entity._generate_cache_key()[0], )
  345. else:
  346. # self.natural_path = parent.natural_path + (entity, )
  347. self.natural_path = self.path
  348. @property
  349. def entity_path(self):
  350. return self
  351. @property
  352. def mapper(self):
  353. return inspection.inspect(self.entity).mapper
  354. def __bool__(self):
  355. return True
  356. __nonzero__ = __bool__
  357. def __getitem__(self, entity):
  358. if isinstance(entity, (int, slice)):
  359. return self.path[entity]
  360. else:
  361. return PropRegistry(self, entity)
  362. class SlotsEntityRegistry(AbstractEntityRegistry):
  363. # for aliased class, return lightweight, no-cycles created
  364. # version
  365. inherit_cache = True
  366. __slots__ = (
  367. "key",
  368. "parent",
  369. "is_aliased_class",
  370. "entity",
  371. "path",
  372. "natural_path",
  373. )
  374. class CachingEntityRegistry(AbstractEntityRegistry, dict):
  375. # for long lived mapper, return dict based caching
  376. # version that creates reference cycles
  377. inherit_cache = True
  378. def __getitem__(self, entity):
  379. if isinstance(entity, (int, slice)):
  380. return self.path[entity]
  381. else:
  382. return dict.__getitem__(self, entity)
  383. def __missing__(self, key):
  384. self[key] = item = PropRegistry(self, key)
  385. return item