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.

1291 lines
46KB

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