dependency.py 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302
  1. # orm/dependency.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. # mypy: ignore-errors
  8. """Relationship dependencies.
  9. """
  10. from __future__ import annotations
  11. from . import attributes
  12. from . import exc
  13. from . import sync
  14. from . import unitofwork
  15. from . import util as mapperutil
  16. from .interfaces import MANYTOMANY
  17. from .interfaces import MANYTOONE
  18. from .interfaces import ONETOMANY
  19. from .. import exc as sa_exc
  20. from .. import sql
  21. from .. import util
  22. class DependencyProcessor:
  23. def __init__(self, prop):
  24. self.prop = prop
  25. self.cascade = prop.cascade
  26. self.mapper = prop.mapper
  27. self.parent = prop.parent
  28. self.secondary = prop.secondary
  29. self.direction = prop.direction
  30. self.post_update = prop.post_update
  31. self.passive_deletes = prop.passive_deletes
  32. self.passive_updates = prop.passive_updates
  33. self.enable_typechecks = prop.enable_typechecks
  34. if self.passive_deletes:
  35. self._passive_delete_flag = attributes.PASSIVE_NO_INITIALIZE
  36. else:
  37. self._passive_delete_flag = attributes.PASSIVE_OFF
  38. if self.passive_updates:
  39. self._passive_update_flag = attributes.PASSIVE_NO_INITIALIZE
  40. else:
  41. self._passive_update_flag = attributes.PASSIVE_OFF
  42. self.sort_key = "%s_%s" % (self.parent._sort_key, prop.key)
  43. self.key = prop.key
  44. if not self.prop.synchronize_pairs:
  45. raise sa_exc.ArgumentError(
  46. "Can't build a DependencyProcessor for relationship %s. "
  47. "No target attributes to populate between parent and "
  48. "child are present" % self.prop
  49. )
  50. @classmethod
  51. def from_relationship(cls, prop):
  52. return _direction_to_processor[prop.direction](prop)
  53. def hasparent(self, state):
  54. """return True if the given object instance has a parent,
  55. according to the ``InstrumentedAttribute`` handled by this
  56. ``DependencyProcessor``.
  57. """
  58. return self.parent.class_manager.get_impl(self.key).hasparent(state)
  59. def per_property_preprocessors(self, uow):
  60. """establish actions and dependencies related to a flush.
  61. These actions will operate on all relevant states in
  62. the aggregate.
  63. """
  64. uow.register_preprocessor(self, True)
  65. def per_property_flush_actions(self, uow):
  66. after_save = unitofwork.ProcessAll(uow, self, False, True)
  67. before_delete = unitofwork.ProcessAll(uow, self, True, True)
  68. parent_saves = unitofwork.SaveUpdateAll(
  69. uow, self.parent.primary_base_mapper
  70. )
  71. child_saves = unitofwork.SaveUpdateAll(
  72. uow, self.mapper.primary_base_mapper
  73. )
  74. parent_deletes = unitofwork.DeleteAll(
  75. uow, self.parent.primary_base_mapper
  76. )
  77. child_deletes = unitofwork.DeleteAll(
  78. uow, self.mapper.primary_base_mapper
  79. )
  80. self.per_property_dependencies(
  81. uow,
  82. parent_saves,
  83. child_saves,
  84. parent_deletes,
  85. child_deletes,
  86. after_save,
  87. before_delete,
  88. )
  89. def per_state_flush_actions(self, uow, states, isdelete):
  90. """establish actions and dependencies related to a flush.
  91. These actions will operate on all relevant states
  92. individually. This occurs only if there are cycles
  93. in the 'aggregated' version of events.
  94. """
  95. child_base_mapper = self.mapper.primary_base_mapper
  96. child_saves = unitofwork.SaveUpdateAll(uow, child_base_mapper)
  97. child_deletes = unitofwork.DeleteAll(uow, child_base_mapper)
  98. # locate and disable the aggregate processors
  99. # for this dependency
  100. if isdelete:
  101. before_delete = unitofwork.ProcessAll(uow, self, True, True)
  102. before_delete.disabled = True
  103. else:
  104. after_save = unitofwork.ProcessAll(uow, self, False, True)
  105. after_save.disabled = True
  106. # check if the "child" side is part of the cycle
  107. if child_saves not in uow.cycles:
  108. # based on the current dependencies we use, the saves/
  109. # deletes should always be in the 'cycles' collection
  110. # together. if this changes, we will have to break up
  111. # this method a bit more.
  112. assert child_deletes not in uow.cycles
  113. # child side is not part of the cycle, so we will link per-state
  114. # actions to the aggregate "saves", "deletes" actions
  115. child_actions = [(child_saves, False), (child_deletes, True)]
  116. child_in_cycles = False
  117. else:
  118. child_in_cycles = True
  119. # check if the "parent" side is part of the cycle
  120. if not isdelete:
  121. parent_saves = unitofwork.SaveUpdateAll(
  122. uow, self.parent.base_mapper
  123. )
  124. parent_deletes = before_delete = None
  125. if parent_saves in uow.cycles:
  126. parent_in_cycles = True
  127. else:
  128. parent_deletes = unitofwork.DeleteAll(uow, self.parent.base_mapper)
  129. parent_saves = after_save = None
  130. if parent_deletes in uow.cycles:
  131. parent_in_cycles = True
  132. # now create actions /dependencies for each state.
  133. for state in states:
  134. # detect if there's anything changed or loaded
  135. # by a preprocessor on this state/attribute. In the
  136. # case of deletes we may try to load missing items here as well.
  137. sum_ = state.manager[self.key].impl.get_all_pending(
  138. state,
  139. state.dict,
  140. self._passive_delete_flag
  141. if isdelete
  142. else attributes.PASSIVE_NO_INITIALIZE,
  143. )
  144. if not sum_:
  145. continue
  146. if isdelete:
  147. before_delete = unitofwork.ProcessState(uow, self, True, state)
  148. if parent_in_cycles:
  149. parent_deletes = unitofwork.DeleteState(uow, state)
  150. else:
  151. after_save = unitofwork.ProcessState(uow, self, False, state)
  152. if parent_in_cycles:
  153. parent_saves = unitofwork.SaveUpdateState(uow, state)
  154. if child_in_cycles:
  155. child_actions = []
  156. for child_state, child in sum_:
  157. if child_state not in uow.states:
  158. child_action = (None, None)
  159. else:
  160. (deleted, listonly) = uow.states[child_state]
  161. if deleted:
  162. child_action = (
  163. unitofwork.DeleteState(uow, child_state),
  164. True,
  165. )
  166. else:
  167. child_action = (
  168. unitofwork.SaveUpdateState(uow, child_state),
  169. False,
  170. )
  171. child_actions.append(child_action)
  172. # establish dependencies between our possibly per-state
  173. # parent action and our possibly per-state child action.
  174. for child_action, childisdelete in child_actions:
  175. self.per_state_dependencies(
  176. uow,
  177. parent_saves,
  178. parent_deletes,
  179. child_action,
  180. after_save,
  181. before_delete,
  182. isdelete,
  183. childisdelete,
  184. )
  185. def presort_deletes(self, uowcommit, states):
  186. return False
  187. def presort_saves(self, uowcommit, states):
  188. return False
  189. def process_deletes(self, uowcommit, states):
  190. pass
  191. def process_saves(self, uowcommit, states):
  192. pass
  193. def prop_has_changes(self, uowcommit, states, isdelete):
  194. if not isdelete or self.passive_deletes:
  195. passive = (
  196. attributes.PASSIVE_NO_INITIALIZE
  197. | attributes.INCLUDE_PENDING_MUTATIONS
  198. )
  199. elif self.direction is MANYTOONE:
  200. # here, we were hoping to optimize having to fetch many-to-one
  201. # for history and ignore it, if there's no further cascades
  202. # to take place. however there are too many less common conditions
  203. # that still take place and tests in test_relationships /
  204. # test_cascade etc. will still fail.
  205. passive = attributes.PASSIVE_NO_FETCH_RELATED
  206. else:
  207. passive = (
  208. attributes.PASSIVE_OFF | attributes.INCLUDE_PENDING_MUTATIONS
  209. )
  210. for s in states:
  211. # TODO: add a high speed method
  212. # to InstanceState which returns: attribute
  213. # has a non-None value, or had one
  214. history = uowcommit.get_attribute_history(s, self.key, passive)
  215. if history and not history.empty():
  216. return True
  217. else:
  218. return (
  219. states
  220. and not self.prop._is_self_referential
  221. and self.mapper in uowcommit.mappers
  222. )
  223. def _verify_canload(self, state):
  224. if self.prop.uselist and state is None:
  225. raise exc.FlushError(
  226. "Can't flush None value found in "
  227. "collection %s" % (self.prop,)
  228. )
  229. elif state is not None and not self.mapper._canload(
  230. state, allow_subtypes=not self.enable_typechecks
  231. ):
  232. if self.mapper._canload(state, allow_subtypes=True):
  233. raise exc.FlushError(
  234. "Attempting to flush an item of type "
  235. "%(x)s as a member of collection "
  236. '"%(y)s". Expected an object of type '
  237. "%(z)s or a polymorphic subclass of "
  238. "this type. If %(x)s is a subclass of "
  239. '%(z)s, configure mapper "%(zm)s" to '
  240. "load this subtype polymorphically, or "
  241. "set enable_typechecks=False to allow "
  242. "any subtype to be accepted for flush. "
  243. % {
  244. "x": state.class_,
  245. "y": self.prop,
  246. "z": self.mapper.class_,
  247. "zm": self.mapper,
  248. }
  249. )
  250. else:
  251. raise exc.FlushError(
  252. "Attempting to flush an item of type "
  253. "%(x)s as a member of collection "
  254. '"%(y)s". Expected an object of type '
  255. "%(z)s or a polymorphic subclass of "
  256. "this type."
  257. % {
  258. "x": state.class_,
  259. "y": self.prop,
  260. "z": self.mapper.class_,
  261. }
  262. )
  263. def _synchronize(self, state, child, associationrow, clearkeys, uowcommit):
  264. raise NotImplementedError()
  265. def _get_reversed_processed_set(self, uow):
  266. if not self.prop._reverse_property:
  267. return None
  268. process_key = tuple(
  269. sorted([self.key] + [p.key for p in self.prop._reverse_property])
  270. )
  271. return uow.memo(("reverse_key", process_key), set)
  272. def _post_update(self, state, uowcommit, related, is_m2o_delete=False):
  273. for x in related:
  274. if not is_m2o_delete or x is not None:
  275. uowcommit.register_post_update(
  276. state, [r for l, r in self.prop.synchronize_pairs]
  277. )
  278. break
  279. def _pks_changed(self, uowcommit, state):
  280. raise NotImplementedError()
  281. def __repr__(self):
  282. return "%s(%s)" % (self.__class__.__name__, self.prop)
  283. class OneToManyDP(DependencyProcessor):
  284. def per_property_dependencies(
  285. self,
  286. uow,
  287. parent_saves,
  288. child_saves,
  289. parent_deletes,
  290. child_deletes,
  291. after_save,
  292. before_delete,
  293. ):
  294. if self.post_update:
  295. child_post_updates = unitofwork.PostUpdateAll(
  296. uow, self.mapper.primary_base_mapper, False
  297. )
  298. child_pre_updates = unitofwork.PostUpdateAll(
  299. uow, self.mapper.primary_base_mapper, True
  300. )
  301. uow.dependencies.update(
  302. [
  303. (child_saves, after_save),
  304. (parent_saves, after_save),
  305. (after_save, child_post_updates),
  306. (before_delete, child_pre_updates),
  307. (child_pre_updates, parent_deletes),
  308. (child_pre_updates, child_deletes),
  309. ]
  310. )
  311. else:
  312. uow.dependencies.update(
  313. [
  314. (parent_saves, after_save),
  315. (after_save, child_saves),
  316. (after_save, child_deletes),
  317. (child_saves, parent_deletes),
  318. (child_deletes, parent_deletes),
  319. (before_delete, child_saves),
  320. (before_delete, child_deletes),
  321. ]
  322. )
  323. def per_state_dependencies(
  324. self,
  325. uow,
  326. save_parent,
  327. delete_parent,
  328. child_action,
  329. after_save,
  330. before_delete,
  331. isdelete,
  332. childisdelete,
  333. ):
  334. if self.post_update:
  335. child_post_updates = unitofwork.PostUpdateAll(
  336. uow, self.mapper.primary_base_mapper, False
  337. )
  338. child_pre_updates = unitofwork.PostUpdateAll(
  339. uow, self.mapper.primary_base_mapper, True
  340. )
  341. # TODO: this whole block is not covered
  342. # by any tests
  343. if not isdelete:
  344. if childisdelete:
  345. uow.dependencies.update(
  346. [
  347. (child_action, after_save),
  348. (after_save, child_post_updates),
  349. ]
  350. )
  351. else:
  352. uow.dependencies.update(
  353. [
  354. (save_parent, after_save),
  355. (child_action, after_save),
  356. (after_save, child_post_updates),
  357. ]
  358. )
  359. else:
  360. if childisdelete:
  361. uow.dependencies.update(
  362. [
  363. (before_delete, child_pre_updates),
  364. (child_pre_updates, delete_parent),
  365. ]
  366. )
  367. else:
  368. uow.dependencies.update(
  369. [
  370. (before_delete, child_pre_updates),
  371. (child_pre_updates, delete_parent),
  372. ]
  373. )
  374. elif not isdelete:
  375. uow.dependencies.update(
  376. [
  377. (save_parent, after_save),
  378. (after_save, child_action),
  379. (save_parent, child_action),
  380. ]
  381. )
  382. else:
  383. uow.dependencies.update(
  384. [(before_delete, child_action), (child_action, delete_parent)]
  385. )
  386. def presort_deletes(self, uowcommit, states):
  387. # head object is being deleted, and we manage its list of
  388. # child objects the child objects have to have their
  389. # foreign key to the parent set to NULL
  390. should_null_fks = (
  391. not self.cascade.delete and not self.passive_deletes == "all"
  392. )
  393. for state in states:
  394. history = uowcommit.get_attribute_history(
  395. state, self.key, self._passive_delete_flag
  396. )
  397. if history:
  398. for child in history.deleted:
  399. if child is not None and self.hasparent(child) is False:
  400. if self.cascade.delete_orphan:
  401. uowcommit.register_object(child, isdelete=True)
  402. else:
  403. uowcommit.register_object(child)
  404. if should_null_fks:
  405. for child in history.unchanged:
  406. if child is not None:
  407. uowcommit.register_object(
  408. child, operation="delete", prop=self.prop
  409. )
  410. def presort_saves(self, uowcommit, states):
  411. children_added = uowcommit.memo(("children_added", self), set)
  412. should_null_fks = (
  413. not self.cascade.delete_orphan
  414. and not self.passive_deletes == "all"
  415. )
  416. for state in states:
  417. pks_changed = self._pks_changed(uowcommit, state)
  418. if not pks_changed or self.passive_updates:
  419. passive = (
  420. attributes.PASSIVE_NO_INITIALIZE
  421. | attributes.INCLUDE_PENDING_MUTATIONS
  422. )
  423. else:
  424. passive = (
  425. attributes.PASSIVE_OFF
  426. | attributes.INCLUDE_PENDING_MUTATIONS
  427. )
  428. history = uowcommit.get_attribute_history(state, self.key, passive)
  429. if history:
  430. for child in history.added:
  431. if child is not None:
  432. uowcommit.register_object(
  433. child,
  434. cancel_delete=True,
  435. operation="add",
  436. prop=self.prop,
  437. )
  438. children_added.update(history.added)
  439. for child in history.deleted:
  440. if not self.cascade.delete_orphan:
  441. if should_null_fks:
  442. uowcommit.register_object(
  443. child,
  444. isdelete=False,
  445. operation="delete",
  446. prop=self.prop,
  447. )
  448. elif self.hasparent(child) is False:
  449. uowcommit.register_object(
  450. child,
  451. isdelete=True,
  452. operation="delete",
  453. prop=self.prop,
  454. )
  455. for c, m, st_, dct_ in self.mapper.cascade_iterator(
  456. "delete", child
  457. ):
  458. uowcommit.register_object(st_, isdelete=True)
  459. if pks_changed:
  460. if history:
  461. for child in history.unchanged:
  462. if child is not None:
  463. uowcommit.register_object(
  464. child,
  465. False,
  466. self.passive_updates,
  467. operation="pk change",
  468. prop=self.prop,
  469. )
  470. def process_deletes(self, uowcommit, states):
  471. # head object is being deleted, and we manage its list of
  472. # child objects the child objects have to have their foreign
  473. # key to the parent set to NULL this phase can be called
  474. # safely for any cascade but is unnecessary if delete cascade
  475. # is on.
  476. if self.post_update or not self.passive_deletes == "all":
  477. children_added = uowcommit.memo(("children_added", self), set)
  478. for state in states:
  479. history = uowcommit.get_attribute_history(
  480. state, self.key, self._passive_delete_flag
  481. )
  482. if history:
  483. for child in history.deleted:
  484. if (
  485. child is not None
  486. and self.hasparent(child) is False
  487. ):
  488. self._synchronize(
  489. state, child, None, True, uowcommit, False
  490. )
  491. if self.post_update and child:
  492. self._post_update(child, uowcommit, [state])
  493. if self.post_update or not self.cascade.delete:
  494. for child in set(history.unchanged).difference(
  495. children_added
  496. ):
  497. if child is not None:
  498. self._synchronize(
  499. state, child, None, True, uowcommit, False
  500. )
  501. if self.post_update and child:
  502. self._post_update(
  503. child, uowcommit, [state]
  504. )
  505. # technically, we can even remove each child from the
  506. # collection here too. but this would be a somewhat
  507. # inconsistent behavior since it wouldn't happen
  508. # if the old parent wasn't deleted but child was moved.
  509. def process_saves(self, uowcommit, states):
  510. should_null_fks = (
  511. not self.cascade.delete_orphan
  512. and not self.passive_deletes == "all"
  513. )
  514. for state in states:
  515. history = uowcommit.get_attribute_history(
  516. state, self.key, attributes.PASSIVE_NO_INITIALIZE
  517. )
  518. if history:
  519. for child in history.added:
  520. self._synchronize(
  521. state, child, None, False, uowcommit, False
  522. )
  523. if child is not None and self.post_update:
  524. self._post_update(child, uowcommit, [state])
  525. for child in history.deleted:
  526. if (
  527. should_null_fks
  528. and not self.cascade.delete_orphan
  529. and not self.hasparent(child)
  530. ):
  531. self._synchronize(
  532. state, child, None, True, uowcommit, False
  533. )
  534. if self._pks_changed(uowcommit, state):
  535. for child in history.unchanged:
  536. self._synchronize(
  537. state, child, None, False, uowcommit, True
  538. )
  539. def _synchronize(
  540. self, state, child, associationrow, clearkeys, uowcommit, pks_changed
  541. ):
  542. source = state
  543. dest = child
  544. self._verify_canload(child)
  545. if dest is None or (
  546. not self.post_update and uowcommit.is_deleted(dest)
  547. ):
  548. return
  549. if clearkeys:
  550. sync.clear(dest, self.mapper, self.prop.synchronize_pairs)
  551. else:
  552. sync.populate(
  553. source,
  554. self.parent,
  555. dest,
  556. self.mapper,
  557. self.prop.synchronize_pairs,
  558. uowcommit,
  559. self.passive_updates and pks_changed,
  560. )
  561. def _pks_changed(self, uowcommit, state):
  562. return sync.source_modified(
  563. uowcommit, state, self.parent, self.prop.synchronize_pairs
  564. )
  565. class ManyToOneDP(DependencyProcessor):
  566. def __init__(self, prop):
  567. DependencyProcessor.__init__(self, prop)
  568. for mapper in self.mapper.self_and_descendants:
  569. mapper._dependency_processors.append(DetectKeySwitch(prop))
  570. def per_property_dependencies(
  571. self,
  572. uow,
  573. parent_saves,
  574. child_saves,
  575. parent_deletes,
  576. child_deletes,
  577. after_save,
  578. before_delete,
  579. ):
  580. if self.post_update:
  581. parent_post_updates = unitofwork.PostUpdateAll(
  582. uow, self.parent.primary_base_mapper, False
  583. )
  584. parent_pre_updates = unitofwork.PostUpdateAll(
  585. uow, self.parent.primary_base_mapper, True
  586. )
  587. uow.dependencies.update(
  588. [
  589. (child_saves, after_save),
  590. (parent_saves, after_save),
  591. (after_save, parent_post_updates),
  592. (after_save, parent_pre_updates),
  593. (before_delete, parent_pre_updates),
  594. (parent_pre_updates, child_deletes),
  595. (parent_pre_updates, parent_deletes),
  596. ]
  597. )
  598. else:
  599. uow.dependencies.update(
  600. [
  601. (child_saves, after_save),
  602. (after_save, parent_saves),
  603. (parent_saves, child_deletes),
  604. (parent_deletes, child_deletes),
  605. ]
  606. )
  607. def per_state_dependencies(
  608. self,
  609. uow,
  610. save_parent,
  611. delete_parent,
  612. child_action,
  613. after_save,
  614. before_delete,
  615. isdelete,
  616. childisdelete,
  617. ):
  618. if self.post_update:
  619. if not isdelete:
  620. parent_post_updates = unitofwork.PostUpdateAll(
  621. uow, self.parent.primary_base_mapper, False
  622. )
  623. if childisdelete:
  624. uow.dependencies.update(
  625. [
  626. (after_save, parent_post_updates),
  627. (parent_post_updates, child_action),
  628. ]
  629. )
  630. else:
  631. uow.dependencies.update(
  632. [
  633. (save_parent, after_save),
  634. (child_action, after_save),
  635. (after_save, parent_post_updates),
  636. ]
  637. )
  638. else:
  639. parent_pre_updates = unitofwork.PostUpdateAll(
  640. uow, self.parent.primary_base_mapper, True
  641. )
  642. uow.dependencies.update(
  643. [
  644. (before_delete, parent_pre_updates),
  645. (parent_pre_updates, delete_parent),
  646. (parent_pre_updates, child_action),
  647. ]
  648. )
  649. elif not isdelete:
  650. if not childisdelete:
  651. uow.dependencies.update(
  652. [(child_action, after_save), (after_save, save_parent)]
  653. )
  654. else:
  655. uow.dependencies.update([(after_save, save_parent)])
  656. else:
  657. if childisdelete:
  658. uow.dependencies.update([(delete_parent, child_action)])
  659. def presort_deletes(self, uowcommit, states):
  660. if self.cascade.delete or self.cascade.delete_orphan:
  661. for state in states:
  662. history = uowcommit.get_attribute_history(
  663. state, self.key, self._passive_delete_flag
  664. )
  665. if history:
  666. if self.cascade.delete_orphan:
  667. todelete = history.sum()
  668. else:
  669. todelete = history.non_deleted()
  670. for child in todelete:
  671. if child is None:
  672. continue
  673. uowcommit.register_object(
  674. child,
  675. isdelete=True,
  676. operation="delete",
  677. prop=self.prop,
  678. )
  679. t = self.mapper.cascade_iterator("delete", child)
  680. for c, m, st_, dct_ in t:
  681. uowcommit.register_object(st_, isdelete=True)
  682. def presort_saves(self, uowcommit, states):
  683. for state in states:
  684. uowcommit.register_object(state, operation="add", prop=self.prop)
  685. if self.cascade.delete_orphan:
  686. history = uowcommit.get_attribute_history(
  687. state, self.key, self._passive_delete_flag
  688. )
  689. if history:
  690. for child in history.deleted:
  691. if self.hasparent(child) is False:
  692. uowcommit.register_object(
  693. child,
  694. isdelete=True,
  695. operation="delete",
  696. prop=self.prop,
  697. )
  698. t = self.mapper.cascade_iterator("delete", child)
  699. for c, m, st_, dct_ in t:
  700. uowcommit.register_object(st_, isdelete=True)
  701. def process_deletes(self, uowcommit, states):
  702. if (
  703. self.post_update
  704. and not self.cascade.delete_orphan
  705. and not self.passive_deletes == "all"
  706. ):
  707. # post_update means we have to update our
  708. # row to not reference the child object
  709. # before we can DELETE the row
  710. for state in states:
  711. self._synchronize(state, None, None, True, uowcommit)
  712. if state and self.post_update:
  713. history = uowcommit.get_attribute_history(
  714. state, self.key, self._passive_delete_flag
  715. )
  716. if history:
  717. self._post_update(
  718. state, uowcommit, history.sum(), is_m2o_delete=True
  719. )
  720. def process_saves(self, uowcommit, states):
  721. for state in states:
  722. history = uowcommit.get_attribute_history(
  723. state, self.key, attributes.PASSIVE_NO_INITIALIZE
  724. )
  725. if history:
  726. if history.added:
  727. for child in history.added:
  728. self._synchronize(
  729. state, child, None, False, uowcommit, "add"
  730. )
  731. elif history.deleted:
  732. self._synchronize(
  733. state, None, None, True, uowcommit, "delete"
  734. )
  735. if self.post_update:
  736. self._post_update(state, uowcommit, history.sum())
  737. def _synchronize(
  738. self,
  739. state,
  740. child,
  741. associationrow,
  742. clearkeys,
  743. uowcommit,
  744. operation=None,
  745. ):
  746. if state is None or (
  747. not self.post_update and uowcommit.is_deleted(state)
  748. ):
  749. return
  750. if (
  751. operation is not None
  752. and child is not None
  753. and not uowcommit.session._contains_state(child)
  754. ):
  755. util.warn(
  756. "Object of type %s not in session, %s "
  757. "operation along '%s' won't proceed"
  758. % (mapperutil.state_class_str(child), operation, self.prop)
  759. )
  760. return
  761. if clearkeys or child is None:
  762. sync.clear(state, self.parent, self.prop.synchronize_pairs)
  763. else:
  764. self._verify_canload(child)
  765. sync.populate(
  766. child,
  767. self.mapper,
  768. state,
  769. self.parent,
  770. self.prop.synchronize_pairs,
  771. uowcommit,
  772. False,
  773. )
  774. class DetectKeySwitch(DependencyProcessor):
  775. """For many-to-one relationships with no one-to-many backref,
  776. searches for parents through the unit of work when a primary
  777. key has changed and updates them.
  778. Theoretically, this approach could be expanded to support transparent
  779. deletion of objects referenced via many-to-one as well, although
  780. the current attribute system doesn't do enough bookkeeping for this
  781. to be efficient.
  782. """
  783. def per_property_preprocessors(self, uow):
  784. if self.prop._reverse_property:
  785. if self.passive_updates:
  786. return
  787. else:
  788. if False in (
  789. prop.passive_updates
  790. for prop in self.prop._reverse_property
  791. ):
  792. return
  793. uow.register_preprocessor(self, False)
  794. def per_property_flush_actions(self, uow):
  795. parent_saves = unitofwork.SaveUpdateAll(uow, self.parent.base_mapper)
  796. after_save = unitofwork.ProcessAll(uow, self, False, False)
  797. uow.dependencies.update([(parent_saves, after_save)])
  798. def per_state_flush_actions(self, uow, states, isdelete):
  799. pass
  800. def presort_deletes(self, uowcommit, states):
  801. pass
  802. def presort_saves(self, uow, states):
  803. if not self.passive_updates:
  804. # for non-passive updates, register in the preprocess stage
  805. # so that mapper save_obj() gets a hold of changes
  806. self._process_key_switches(states, uow)
  807. def prop_has_changes(self, uow, states, isdelete):
  808. if not isdelete and self.passive_updates:
  809. d = self._key_switchers(uow, states)
  810. return bool(d)
  811. return False
  812. def process_deletes(self, uowcommit, states):
  813. assert False
  814. def process_saves(self, uowcommit, states):
  815. # for passive updates, register objects in the process stage
  816. # so that we avoid ManyToOneDP's registering the object without
  817. # the listonly flag in its own preprocess stage (results in UPDATE)
  818. # statements being emitted
  819. assert self.passive_updates
  820. self._process_key_switches(states, uowcommit)
  821. def _key_switchers(self, uow, states):
  822. switched, notswitched = uow.memo(
  823. ("pk_switchers", self), lambda: (set(), set())
  824. )
  825. allstates = switched.union(notswitched)
  826. for s in states:
  827. if s not in allstates:
  828. if self._pks_changed(uow, s):
  829. switched.add(s)
  830. else:
  831. notswitched.add(s)
  832. return switched
  833. def _process_key_switches(self, deplist, uowcommit):
  834. switchers = self._key_switchers(uowcommit, deplist)
  835. if switchers:
  836. # if primary key values have actually changed somewhere, perform
  837. # a linear search through the UOW in search of a parent.
  838. for state in uowcommit.session.identity_map.all_states():
  839. if not issubclass(state.class_, self.parent.class_):
  840. continue
  841. dict_ = state.dict
  842. related = state.get_impl(self.key).get(
  843. state, dict_, passive=self._passive_update_flag
  844. )
  845. if (
  846. related is not attributes.PASSIVE_NO_RESULT
  847. and related is not None
  848. ):
  849. if self.prop.uselist:
  850. if not related:
  851. continue
  852. related_obj = related[0]
  853. else:
  854. related_obj = related
  855. related_state = attributes.instance_state(related_obj)
  856. if related_state in switchers:
  857. uowcommit.register_object(
  858. state, False, self.passive_updates
  859. )
  860. sync.populate(
  861. related_state,
  862. self.mapper,
  863. state,
  864. self.parent,
  865. self.prop.synchronize_pairs,
  866. uowcommit,
  867. self.passive_updates,
  868. )
  869. def _pks_changed(self, uowcommit, state):
  870. return bool(state.key) and sync.source_modified(
  871. uowcommit, state, self.mapper, self.prop.synchronize_pairs
  872. )
  873. class ManyToManyDP(DependencyProcessor):
  874. def per_property_dependencies(
  875. self,
  876. uow,
  877. parent_saves,
  878. child_saves,
  879. parent_deletes,
  880. child_deletes,
  881. after_save,
  882. before_delete,
  883. ):
  884. uow.dependencies.update(
  885. [
  886. (parent_saves, after_save),
  887. (child_saves, after_save),
  888. (after_save, child_deletes),
  889. # a rowswitch on the parent from deleted to saved
  890. # can make this one occur, as the "save" may remove
  891. # an element from the
  892. # "deleted" list before we have a chance to
  893. # process its child rows
  894. (before_delete, parent_saves),
  895. (before_delete, parent_deletes),
  896. (before_delete, child_deletes),
  897. (before_delete, child_saves),
  898. ]
  899. )
  900. def per_state_dependencies(
  901. self,
  902. uow,
  903. save_parent,
  904. delete_parent,
  905. child_action,
  906. after_save,
  907. before_delete,
  908. isdelete,
  909. childisdelete,
  910. ):
  911. if not isdelete:
  912. if childisdelete:
  913. uow.dependencies.update(
  914. [(save_parent, after_save), (after_save, child_action)]
  915. )
  916. else:
  917. uow.dependencies.update(
  918. [(save_parent, after_save), (child_action, after_save)]
  919. )
  920. else:
  921. uow.dependencies.update(
  922. [(before_delete, child_action), (before_delete, delete_parent)]
  923. )
  924. def presort_deletes(self, uowcommit, states):
  925. # TODO: no tests fail if this whole
  926. # thing is removed !!!!
  927. if not self.passive_deletes:
  928. # if no passive deletes, load history on
  929. # the collection, so that prop_has_changes()
  930. # returns True
  931. for state in states:
  932. uowcommit.get_attribute_history(
  933. state, self.key, self._passive_delete_flag
  934. )
  935. def presort_saves(self, uowcommit, states):
  936. if not self.passive_updates:
  937. # if no passive updates, load history on
  938. # each collection where parent has changed PK,
  939. # so that prop_has_changes() returns True
  940. for state in states:
  941. if self._pks_changed(uowcommit, state):
  942. history = uowcommit.get_attribute_history(
  943. state, self.key, attributes.PASSIVE_OFF
  944. )
  945. if not self.cascade.delete_orphan:
  946. return
  947. # check for child items removed from the collection
  948. # if delete_orphan check is turned on.
  949. for state in states:
  950. history = uowcommit.get_attribute_history(
  951. state, self.key, attributes.PASSIVE_NO_INITIALIZE
  952. )
  953. if history:
  954. for child in history.deleted:
  955. if self.hasparent(child) is False:
  956. uowcommit.register_object(
  957. child,
  958. isdelete=True,
  959. operation="delete",
  960. prop=self.prop,
  961. )
  962. for c, m, st_, dct_ in self.mapper.cascade_iterator(
  963. "delete", child
  964. ):
  965. uowcommit.register_object(st_, isdelete=True)
  966. def process_deletes(self, uowcommit, states):
  967. secondary_delete = []
  968. secondary_insert = []
  969. secondary_update = []
  970. processed = self._get_reversed_processed_set(uowcommit)
  971. tmp = set()
  972. for state in states:
  973. # this history should be cached already, as
  974. # we loaded it in preprocess_deletes
  975. history = uowcommit.get_attribute_history(
  976. state, self.key, self._passive_delete_flag
  977. )
  978. if history:
  979. for child in history.non_added():
  980. if child is None or (
  981. processed is not None and (state, child) in processed
  982. ):
  983. continue
  984. associationrow = {}
  985. if not self._synchronize(
  986. state,
  987. child,
  988. associationrow,
  989. False,
  990. uowcommit,
  991. "delete",
  992. ):
  993. continue
  994. secondary_delete.append(associationrow)
  995. tmp.update((c, state) for c in history.non_added())
  996. if processed is not None:
  997. processed.update(tmp)
  998. self._run_crud(
  999. uowcommit, secondary_insert, secondary_update, secondary_delete
  1000. )
  1001. def process_saves(self, uowcommit, states):
  1002. secondary_delete = []
  1003. secondary_insert = []
  1004. secondary_update = []
  1005. processed = self._get_reversed_processed_set(uowcommit)
  1006. tmp = set()
  1007. for state in states:
  1008. need_cascade_pks = not self.passive_updates and self._pks_changed(
  1009. uowcommit, state
  1010. )
  1011. if need_cascade_pks:
  1012. passive = (
  1013. attributes.PASSIVE_OFF
  1014. | attributes.INCLUDE_PENDING_MUTATIONS
  1015. )
  1016. else:
  1017. passive = (
  1018. attributes.PASSIVE_NO_INITIALIZE
  1019. | attributes.INCLUDE_PENDING_MUTATIONS
  1020. )
  1021. history = uowcommit.get_attribute_history(state, self.key, passive)
  1022. if history:
  1023. for child in history.added:
  1024. if processed is not None and (state, child) in processed:
  1025. continue
  1026. associationrow = {}
  1027. if not self._synchronize(
  1028. state, child, associationrow, False, uowcommit, "add"
  1029. ):
  1030. continue
  1031. secondary_insert.append(associationrow)
  1032. for child in history.deleted:
  1033. if processed is not None and (state, child) in processed:
  1034. continue
  1035. associationrow = {}
  1036. if not self._synchronize(
  1037. state,
  1038. child,
  1039. associationrow,
  1040. False,
  1041. uowcommit,
  1042. "delete",
  1043. ):
  1044. continue
  1045. secondary_delete.append(associationrow)
  1046. tmp.update((c, state) for c in history.added + history.deleted)
  1047. if need_cascade_pks:
  1048. for child in history.unchanged:
  1049. associationrow = {}
  1050. sync.update(
  1051. state,
  1052. self.parent,
  1053. associationrow,
  1054. "old_",
  1055. self.prop.synchronize_pairs,
  1056. )
  1057. sync.update(
  1058. child,
  1059. self.mapper,
  1060. associationrow,
  1061. "old_",
  1062. self.prop.secondary_synchronize_pairs,
  1063. )
  1064. secondary_update.append(associationrow)
  1065. if processed is not None:
  1066. processed.update(tmp)
  1067. self._run_crud(
  1068. uowcommit, secondary_insert, secondary_update, secondary_delete
  1069. )
  1070. def _run_crud(
  1071. self, uowcommit, secondary_insert, secondary_update, secondary_delete
  1072. ):
  1073. connection = uowcommit.transaction.connection(self.mapper)
  1074. if secondary_delete:
  1075. associationrow = secondary_delete[0]
  1076. statement = self.secondary.delete().where(
  1077. sql.and_(
  1078. *[
  1079. c == sql.bindparam(c.key, type_=c.type)
  1080. for c in self.secondary.c
  1081. if c.key in associationrow
  1082. ]
  1083. )
  1084. )
  1085. result = connection.execute(statement, secondary_delete)
  1086. if (
  1087. result.supports_sane_multi_rowcount()
  1088. ) and result.rowcount != len(secondary_delete):
  1089. raise exc.StaleDataError(
  1090. "DELETE statement on table '%s' expected to delete "
  1091. "%d row(s); Only %d were matched."
  1092. % (
  1093. self.secondary.description,
  1094. len(secondary_delete),
  1095. result.rowcount,
  1096. )
  1097. )
  1098. if secondary_update:
  1099. associationrow = secondary_update[0]
  1100. statement = self.secondary.update().where(
  1101. sql.and_(
  1102. *[
  1103. c == sql.bindparam("old_" + c.key, type_=c.type)
  1104. for c in self.secondary.c
  1105. if c.key in associationrow
  1106. ]
  1107. )
  1108. )
  1109. result = connection.execute(statement, secondary_update)
  1110. if (
  1111. result.supports_sane_multi_rowcount()
  1112. ) and result.rowcount != len(secondary_update):
  1113. raise exc.StaleDataError(
  1114. "UPDATE statement on table '%s' expected to update "
  1115. "%d row(s); Only %d were matched."
  1116. % (
  1117. self.secondary.description,
  1118. len(secondary_update),
  1119. result.rowcount,
  1120. )
  1121. )
  1122. if secondary_insert:
  1123. statement = self.secondary.insert()
  1124. connection.execute(statement, secondary_insert)
  1125. def _synchronize(
  1126. self, state, child, associationrow, clearkeys, uowcommit, operation
  1127. ):
  1128. # this checks for None if uselist=True
  1129. self._verify_canload(child)
  1130. # but if uselist=False we get here. If child is None,
  1131. # no association row can be generated, so return.
  1132. if child is None:
  1133. return False
  1134. if child is not None and not uowcommit.session._contains_state(child):
  1135. if not child.deleted:
  1136. util.warn(
  1137. "Object of type %s not in session, %s "
  1138. "operation along '%s' won't proceed"
  1139. % (mapperutil.state_class_str(child), operation, self.prop)
  1140. )
  1141. return False
  1142. sync.populate_dict(
  1143. state, self.parent, associationrow, self.prop.synchronize_pairs
  1144. )
  1145. sync.populate_dict(
  1146. child,
  1147. self.mapper,
  1148. associationrow,
  1149. self.prop.secondary_synchronize_pairs,
  1150. )
  1151. return True
  1152. def _pks_changed(self, uowcommit, state):
  1153. return sync.source_modified(
  1154. uowcommit, state, self.parent, self.prop.synchronize_pairs
  1155. )
  1156. _direction_to_processor = {
  1157. ONETOMANY: OneToManyDP,
  1158. MANYTOONE: ManyToOneDP,
  1159. MANYTOMANY: ManyToManyDP,
  1160. }