Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# orm/dependency.py 

2# Copyright (C) 2005-2020 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 

8"""Relationship dependencies. 

9 

10""" 

11 

12from . import attributes 

13from . import exc 

14from . import sync 

15from . import unitofwork 

16from . import util as mapperutil 

17from .interfaces import MANYTOMANY 

18from .interfaces import MANYTOONE 

19from .interfaces import ONETOMANY 

20from .. import exc as sa_exc 

21from .. import sql 

22from .. import util 

23 

24 

25class DependencyProcessor(object): 

26 def __init__(self, prop): 

27 self.prop = prop 

28 self.cascade = prop.cascade 

29 self.mapper = prop.mapper 

30 self.parent = prop.parent 

31 self.secondary = prop.secondary 

32 self.direction = prop.direction 

33 self.post_update = prop.post_update 

34 self.passive_deletes = prop.passive_deletes 

35 self.passive_updates = prop.passive_updates 

36 self.enable_typechecks = prop.enable_typechecks 

37 if self.passive_deletes: 

38 self._passive_delete_flag = attributes.PASSIVE_NO_INITIALIZE 

39 else: 

40 self._passive_delete_flag = attributes.PASSIVE_OFF 

41 if self.passive_updates: 

42 self._passive_update_flag = attributes.PASSIVE_NO_INITIALIZE 

43 else: 

44 self._passive_update_flag = attributes.PASSIVE_OFF 

45 

46 self.key = prop.key 

47 if not self.prop.synchronize_pairs: 

48 raise sa_exc.ArgumentError( 

49 "Can't build a DependencyProcessor for relationship %s. " 

50 "No target attributes to populate between parent and " 

51 "child are present" % self.prop 

52 ) 

53 

54 @classmethod 

55 def from_relationship(cls, prop): 

56 return _direction_to_processor[prop.direction](prop) 

57 

58 def hasparent(self, state): 

59 """return True if the given object instance has a parent, 

60 according to the ``InstrumentedAttribute`` handled by this 

61 ``DependencyProcessor``. 

62 

63 """ 

64 return self.parent.class_manager.get_impl(self.key).hasparent(state) 

65 

66 def per_property_preprocessors(self, uow): 

67 """establish actions and dependencies related to a flush. 

68 

69 These actions will operate on all relevant states in 

70 the aggregate. 

71 

72 """ 

73 uow.register_preprocessor(self, True) 

74 

75 def per_property_flush_actions(self, uow): 

76 after_save = unitofwork.ProcessAll(uow, self, False, True) 

77 before_delete = unitofwork.ProcessAll(uow, self, True, True) 

78 

79 parent_saves = unitofwork.SaveUpdateAll( 

80 uow, self.parent.primary_base_mapper 

81 ) 

82 child_saves = unitofwork.SaveUpdateAll( 

83 uow, self.mapper.primary_base_mapper 

84 ) 

85 

86 parent_deletes = unitofwork.DeleteAll( 

87 uow, self.parent.primary_base_mapper 

88 ) 

89 child_deletes = unitofwork.DeleteAll( 

90 uow, self.mapper.primary_base_mapper 

91 ) 

92 

93 self.per_property_dependencies( 

94 uow, 

95 parent_saves, 

96 child_saves, 

97 parent_deletes, 

98 child_deletes, 

99 after_save, 

100 before_delete, 

101 ) 

102 

103 def per_state_flush_actions(self, uow, states, isdelete): 

104 """establish actions and dependencies related to a flush. 

105 

106 These actions will operate on all relevant states 

107 individually. This occurs only if there are cycles 

108 in the 'aggregated' version of events. 

109 

110 """ 

111 

112 child_base_mapper = self.mapper.primary_base_mapper 

113 child_saves = unitofwork.SaveUpdateAll(uow, child_base_mapper) 

114 child_deletes = unitofwork.DeleteAll(uow, child_base_mapper) 

115 

116 # locate and disable the aggregate processors 

117 # for this dependency 

118 

119 if isdelete: 

120 before_delete = unitofwork.ProcessAll(uow, self, True, True) 

121 before_delete.disabled = True 

122 else: 

123 after_save = unitofwork.ProcessAll(uow, self, False, True) 

124 after_save.disabled = True 

125 

126 # check if the "child" side is part of the cycle 

127 

128 if child_saves not in uow.cycles: 

129 # based on the current dependencies we use, the saves/ 

130 # deletes should always be in the 'cycles' collection 

131 # together. if this changes, we will have to break up 

132 # this method a bit more. 

133 assert child_deletes not in uow.cycles 

134 

135 # child side is not part of the cycle, so we will link per-state 

136 # actions to the aggregate "saves", "deletes" actions 

137 child_actions = [(child_saves, False), (child_deletes, True)] 

138 child_in_cycles = False 

139 else: 

140 child_in_cycles = True 

141 

142 # check if the "parent" side is part of the cycle 

143 if not isdelete: 

144 parent_saves = unitofwork.SaveUpdateAll( 

145 uow, self.parent.base_mapper 

146 ) 

147 parent_deletes = before_delete = None 

148 if parent_saves in uow.cycles: 

149 parent_in_cycles = True 

150 else: 

151 parent_deletes = unitofwork.DeleteAll(uow, self.parent.base_mapper) 

152 parent_saves = after_save = None 

153 if parent_deletes in uow.cycles: 

154 parent_in_cycles = True 

155 

156 # now create actions /dependencies for each state. 

157 

158 for state in states: 

159 # detect if there's anything changed or loaded 

160 # by a preprocessor on this state/attribute. In the 

161 # case of deletes we may try to load missing items here as well. 

162 sum_ = state.manager[self.key].impl.get_all_pending( 

163 state, 

164 state.dict, 

165 self._passive_delete_flag 

166 if isdelete 

167 else attributes.PASSIVE_NO_INITIALIZE, 

168 ) 

169 

170 if not sum_: 

171 continue 

172 

173 if isdelete: 

174 before_delete = unitofwork.ProcessState(uow, self, True, state) 

175 if parent_in_cycles: 

176 parent_deletes = unitofwork.DeleteState(uow, state) 

177 else: 

178 after_save = unitofwork.ProcessState(uow, self, False, state) 

179 if parent_in_cycles: 

180 parent_saves = unitofwork.SaveUpdateState(uow, state) 

181 

182 if child_in_cycles: 

183 child_actions = [] 

184 for child_state, child in sum_: 

185 if child_state not in uow.states: 

186 child_action = (None, None) 

187 else: 

188 (deleted, listonly) = uow.states[child_state] 

189 if deleted: 

190 child_action = ( 

191 unitofwork.DeleteState(uow, child_state), 

192 True, 

193 ) 

194 else: 

195 child_action = ( 

196 unitofwork.SaveUpdateState(uow, child_state), 

197 False, 

198 ) 

199 child_actions.append(child_action) 

200 

201 # establish dependencies between our possibly per-state 

202 # parent action and our possibly per-state child action. 

203 for child_action, childisdelete in child_actions: 

204 self.per_state_dependencies( 

205 uow, 

206 parent_saves, 

207 parent_deletes, 

208 child_action, 

209 after_save, 

210 before_delete, 

211 isdelete, 

212 childisdelete, 

213 ) 

214 

215 def presort_deletes(self, uowcommit, states): 

216 return False 

217 

218 def presort_saves(self, uowcommit, states): 

219 return False 

220 

221 def process_deletes(self, uowcommit, states): 

222 pass 

223 

224 def process_saves(self, uowcommit, states): 

225 pass 

226 

227 def prop_has_changes(self, uowcommit, states, isdelete): 

228 if not isdelete or self.passive_deletes: 

229 passive = attributes.PASSIVE_NO_INITIALIZE 

230 elif self.direction is MANYTOONE: 

231 passive = attributes.PASSIVE_NO_FETCH_RELATED 

232 else: 

233 passive = attributes.PASSIVE_OFF 

234 

235 for s in states: 

236 # TODO: add a high speed method 

237 # to InstanceState which returns: attribute 

238 # has a non-None value, or had one 

239 history = uowcommit.get_attribute_history(s, self.key, passive) 

240 if history and not history.empty(): 

241 return True 

242 else: 

243 return ( 

244 states 

245 and not self.prop._is_self_referential 

246 and self.mapper in uowcommit.mappers 

247 ) 

248 

249 def _verify_canload(self, state): 

250 if self.prop.uselist and state is None: 

251 raise exc.FlushError( 

252 "Can't flush None value found in " 

253 "collection %s" % (self.prop,) 

254 ) 

255 elif state is not None and not self.mapper._canload( 

256 state, allow_subtypes=not self.enable_typechecks 

257 ): 

258 if self.mapper._canload(state, allow_subtypes=True): 

259 raise exc.FlushError( 

260 "Attempting to flush an item of type " 

261 "%(x)s as a member of collection " 

262 '"%(y)s". Expected an object of type ' 

263 "%(z)s or a polymorphic subclass of " 

264 "this type. If %(x)s is a subclass of " 

265 '%(z)s, configure mapper "%(zm)s" to ' 

266 "load this subtype polymorphically, or " 

267 "set enable_typechecks=False to allow " 

268 "any subtype to be accepted for flush. " 

269 % { 

270 "x": state.class_, 

271 "y": self.prop, 

272 "z": self.mapper.class_, 

273 "zm": self.mapper, 

274 } 

275 ) 

276 else: 

277 raise exc.FlushError( 

278 "Attempting to flush an item of type " 

279 "%(x)s as a member of collection " 

280 '"%(y)s". Expected an object of type ' 

281 "%(z)s or a polymorphic subclass of " 

282 "this type." 

283 % { 

284 "x": state.class_, 

285 "y": self.prop, 

286 "z": self.mapper.class_, 

287 } 

288 ) 

289 

290 def _synchronize(self, state, child, associationrow, clearkeys, uowcommit): 

291 raise NotImplementedError() 

292 

293 def _get_reversed_processed_set(self, uow): 

294 if not self.prop._reverse_property: 

295 return None 

296 

297 process_key = tuple( 

298 sorted([self.key] + [p.key for p in self.prop._reverse_property]) 

299 ) 

300 return uow.memo(("reverse_key", process_key), set) 

301 

302 def _post_update(self, state, uowcommit, related, is_m2o_delete=False): 

303 for x in related: 

304 if not is_m2o_delete or x is not None: 

305 uowcommit.register_post_update( 

306 state, [r for l, r in self.prop.synchronize_pairs] 

307 ) 

308 break 

309 

310 def _pks_changed(self, uowcommit, state): 

311 raise NotImplementedError() 

312 

313 def __repr__(self): 

314 return "%s(%s)" % (self.__class__.__name__, self.prop) 

315 

316 

317class OneToManyDP(DependencyProcessor): 

318 def per_property_dependencies( 

319 self, 

320 uow, 

321 parent_saves, 

322 child_saves, 

323 parent_deletes, 

324 child_deletes, 

325 after_save, 

326 before_delete, 

327 ): 

328 if self.post_update: 

329 child_post_updates = unitofwork.PostUpdateAll( 

330 uow, self.mapper.primary_base_mapper, False 

331 ) 

332 child_pre_updates = unitofwork.PostUpdateAll( 

333 uow, self.mapper.primary_base_mapper, True 

334 ) 

335 

336 uow.dependencies.update( 

337 [ 

338 (child_saves, after_save), 

339 (parent_saves, after_save), 

340 (after_save, child_post_updates), 

341 (before_delete, child_pre_updates), 

342 (child_pre_updates, parent_deletes), 

343 (child_pre_updates, child_deletes), 

344 ] 

345 ) 

346 else: 

347 uow.dependencies.update( 

348 [ 

349 (parent_saves, after_save), 

350 (after_save, child_saves), 

351 (after_save, child_deletes), 

352 (child_saves, parent_deletes), 

353 (child_deletes, parent_deletes), 

354 (before_delete, child_saves), 

355 (before_delete, child_deletes), 

356 ] 

357 ) 

358 

359 def per_state_dependencies( 

360 self, 

361 uow, 

362 save_parent, 

363 delete_parent, 

364 child_action, 

365 after_save, 

366 before_delete, 

367 isdelete, 

368 childisdelete, 

369 ): 

370 

371 if self.post_update: 

372 

373 child_post_updates = unitofwork.PostUpdateAll( 

374 uow, self.mapper.primary_base_mapper, False 

375 ) 

376 child_pre_updates = unitofwork.PostUpdateAll( 

377 uow, self.mapper.primary_base_mapper, True 

378 ) 

379 

380 # TODO: this whole block is not covered 

381 # by any tests 

382 if not isdelete: 

383 if childisdelete: 

384 uow.dependencies.update( 

385 [ 

386 (child_action, after_save), 

387 (after_save, child_post_updates), 

388 ] 

389 ) 

390 else: 

391 uow.dependencies.update( 

392 [ 

393 (save_parent, after_save), 

394 (child_action, after_save), 

395 (after_save, child_post_updates), 

396 ] 

397 ) 

398 else: 

399 if childisdelete: 

400 uow.dependencies.update( 

401 [ 

402 (before_delete, child_pre_updates), 

403 (child_pre_updates, delete_parent), 

404 ] 

405 ) 

406 else: 

407 uow.dependencies.update( 

408 [ 

409 (before_delete, child_pre_updates), 

410 (child_pre_updates, delete_parent), 

411 ] 

412 ) 

413 elif not isdelete: 

414 uow.dependencies.update( 

415 [ 

416 (save_parent, after_save), 

417 (after_save, child_action), 

418 (save_parent, child_action), 

419 ] 

420 ) 

421 else: 

422 uow.dependencies.update( 

423 [(before_delete, child_action), (child_action, delete_parent)] 

424 ) 

425 

426 def presort_deletes(self, uowcommit, states): 

427 # head object is being deleted, and we manage its list of 

428 # child objects the child objects have to have their 

429 # foreign key to the parent set to NULL 

430 should_null_fks = ( 

431 not self.cascade.delete and not self.passive_deletes == "all" 

432 ) 

433 

434 for state in states: 

435 history = uowcommit.get_attribute_history( 

436 state, self.key, self._passive_delete_flag 

437 ) 

438 if history: 

439 for child in history.deleted: 

440 if child is not None and self.hasparent(child) is False: 

441 if self.cascade.delete_orphan: 

442 uowcommit.register_object(child, isdelete=True) 

443 else: 

444 uowcommit.register_object(child) 

445 

446 if should_null_fks: 

447 for child in history.unchanged: 

448 if child is not None: 

449 uowcommit.register_object( 

450 child, operation="delete", prop=self.prop 

451 ) 

452 

453 def presort_saves(self, uowcommit, states): 

454 children_added = uowcommit.memo(("children_added", self), set) 

455 

456 should_null_fks = ( 

457 not self.cascade.delete_orphan 

458 and not self.passive_deletes == "all" 

459 ) 

460 

461 for state in states: 

462 pks_changed = self._pks_changed(uowcommit, state) 

463 

464 if not pks_changed or self.passive_updates: 

465 passive = attributes.PASSIVE_NO_INITIALIZE 

466 else: 

467 passive = attributes.PASSIVE_OFF 

468 

469 history = uowcommit.get_attribute_history(state, self.key, passive) 

470 if history: 

471 for child in history.added: 

472 if child is not None: 

473 uowcommit.register_object( 

474 child, 

475 cancel_delete=True, 

476 operation="add", 

477 prop=self.prop, 

478 ) 

479 

480 children_added.update(history.added) 

481 

482 for child in history.deleted: 

483 if not self.cascade.delete_orphan: 

484 if should_null_fks: 

485 uowcommit.register_object( 

486 child, 

487 isdelete=False, 

488 operation="delete", 

489 prop=self.prop, 

490 ) 

491 elif self.hasparent(child) is False: 

492 uowcommit.register_object( 

493 child, 

494 isdelete=True, 

495 operation="delete", 

496 prop=self.prop, 

497 ) 

498 for c, m, st_, dct_ in self.mapper.cascade_iterator( 

499 "delete", child 

500 ): 

501 uowcommit.register_object(st_, isdelete=True) 

502 

503 if pks_changed: 

504 if history: 

505 for child in history.unchanged: 

506 if child is not None: 

507 uowcommit.register_object( 

508 child, 

509 False, 

510 self.passive_updates, 

511 operation="pk change", 

512 prop=self.prop, 

513 ) 

514 

515 def process_deletes(self, uowcommit, states): 

516 # head object is being deleted, and we manage its list of 

517 # child objects the child objects have to have their foreign 

518 # key to the parent set to NULL this phase can be called 

519 # safely for any cascade but is unnecessary if delete cascade 

520 # is on. 

521 

522 if self.post_update or not self.passive_deletes == "all": 

523 children_added = uowcommit.memo(("children_added", self), set) 

524 

525 for state in states: 

526 history = uowcommit.get_attribute_history( 

527 state, self.key, self._passive_delete_flag 

528 ) 

529 if history: 

530 for child in history.deleted: 

531 if ( 

532 child is not None 

533 and self.hasparent(child) is False 

534 ): 

535 self._synchronize( 

536 state, child, None, True, uowcommit, False 

537 ) 

538 if self.post_update and child: 

539 self._post_update(child, uowcommit, [state]) 

540 

541 if self.post_update or not self.cascade.delete: 

542 for child in set(history.unchanged).difference( 

543 children_added 

544 ): 

545 if child is not None: 

546 self._synchronize( 

547 state, child, None, True, uowcommit, False 

548 ) 

549 if self.post_update and child: 

550 self._post_update( 

551 child, uowcommit, [state] 

552 ) 

553 

554 # technically, we can even remove each child from the 

555 # collection here too. but this would be a somewhat 

556 # inconsistent behavior since it wouldn't happen 

557 # if the old parent wasn't deleted but child was moved. 

558 

559 def process_saves(self, uowcommit, states): 

560 should_null_fks = ( 

561 not self.cascade.delete_orphan 

562 and not self.passive_deletes == "all" 

563 ) 

564 

565 for state in states: 

566 history = uowcommit.get_attribute_history( 

567 state, self.key, attributes.PASSIVE_NO_INITIALIZE 

568 ) 

569 if history: 

570 for child in history.added: 

571 self._synchronize( 

572 state, child, None, False, uowcommit, False 

573 ) 

574 if child is not None and self.post_update: 

575 self._post_update(child, uowcommit, [state]) 

576 

577 for child in history.deleted: 

578 if ( 

579 should_null_fks 

580 and not self.cascade.delete_orphan 

581 and not self.hasparent(child) 

582 ): 

583 self._synchronize( 

584 state, child, None, True, uowcommit, False 

585 ) 

586 

587 if self._pks_changed(uowcommit, state): 

588 for child in history.unchanged: 

589 self._synchronize( 

590 state, child, None, False, uowcommit, True 

591 ) 

592 

593 def _synchronize( 

594 self, state, child, associationrow, clearkeys, uowcommit, pks_changed 

595 ): 

596 source = state 

597 dest = child 

598 self._verify_canload(child) 

599 if dest is None or ( 

600 not self.post_update and uowcommit.is_deleted(dest) 

601 ): 

602 return 

603 if clearkeys: 

604 sync.clear(dest, self.mapper, self.prop.synchronize_pairs) 

605 else: 

606 sync.populate( 

607 source, 

608 self.parent, 

609 dest, 

610 self.mapper, 

611 self.prop.synchronize_pairs, 

612 uowcommit, 

613 self.passive_updates and pks_changed, 

614 ) 

615 

616 def _pks_changed(self, uowcommit, state): 

617 return sync.source_modified( 

618 uowcommit, state, self.parent, self.prop.synchronize_pairs 

619 ) 

620 

621 

622class ManyToOneDP(DependencyProcessor): 

623 def __init__(self, prop): 

624 DependencyProcessor.__init__(self, prop) 

625 for mapper in self.mapper.self_and_descendants: 

626 mapper._dependency_processors.append(DetectKeySwitch(prop)) 

627 

628 def per_property_dependencies( 

629 self, 

630 uow, 

631 parent_saves, 

632 child_saves, 

633 parent_deletes, 

634 child_deletes, 

635 after_save, 

636 before_delete, 

637 ): 

638 

639 if self.post_update: 

640 parent_post_updates = unitofwork.PostUpdateAll( 

641 uow, self.parent.primary_base_mapper, False 

642 ) 

643 parent_pre_updates = unitofwork.PostUpdateAll( 

644 uow, self.parent.primary_base_mapper, True 

645 ) 

646 

647 uow.dependencies.update( 

648 [ 

649 (child_saves, after_save), 

650 (parent_saves, after_save), 

651 (after_save, parent_post_updates), 

652 (after_save, parent_pre_updates), 

653 (before_delete, parent_pre_updates), 

654 (parent_pre_updates, child_deletes), 

655 (parent_pre_updates, parent_deletes), 

656 ] 

657 ) 

658 else: 

659 uow.dependencies.update( 

660 [ 

661 (child_saves, after_save), 

662 (after_save, parent_saves), 

663 (parent_saves, child_deletes), 

664 (parent_deletes, child_deletes), 

665 ] 

666 ) 

667 

668 def per_state_dependencies( 

669 self, 

670 uow, 

671 save_parent, 

672 delete_parent, 

673 child_action, 

674 after_save, 

675 before_delete, 

676 isdelete, 

677 childisdelete, 

678 ): 

679 

680 if self.post_update: 

681 

682 if not isdelete: 

683 parent_post_updates = unitofwork.PostUpdateAll( 

684 uow, self.parent.primary_base_mapper, False 

685 ) 

686 if childisdelete: 

687 uow.dependencies.update( 

688 [ 

689 (after_save, parent_post_updates), 

690 (parent_post_updates, child_action), 

691 ] 

692 ) 

693 else: 

694 uow.dependencies.update( 

695 [ 

696 (save_parent, after_save), 

697 (child_action, after_save), 

698 (after_save, parent_post_updates), 

699 ] 

700 ) 

701 else: 

702 parent_pre_updates = unitofwork.PostUpdateAll( 

703 uow, self.parent.primary_base_mapper, True 

704 ) 

705 

706 uow.dependencies.update( 

707 [ 

708 (before_delete, parent_pre_updates), 

709 (parent_pre_updates, delete_parent), 

710 (parent_pre_updates, child_action), 

711 ] 

712 ) 

713 

714 elif not isdelete: 

715 if not childisdelete: 

716 uow.dependencies.update( 

717 [(child_action, after_save), (after_save, save_parent)] 

718 ) 

719 else: 

720 uow.dependencies.update([(after_save, save_parent)]) 

721 

722 else: 

723 if childisdelete: 

724 uow.dependencies.update([(delete_parent, child_action)]) 

725 

726 def presort_deletes(self, uowcommit, states): 

727 if self.cascade.delete or self.cascade.delete_orphan: 

728 for state in states: 

729 history = uowcommit.get_attribute_history( 

730 state, self.key, self._passive_delete_flag 

731 ) 

732 if history: 

733 if self.cascade.delete_orphan: 

734 todelete = history.sum() 

735 else: 

736 todelete = history.non_deleted() 

737 for child in todelete: 

738 if child is None: 

739 continue 

740 uowcommit.register_object( 

741 child, 

742 isdelete=True, 

743 operation="delete", 

744 prop=self.prop, 

745 ) 

746 t = self.mapper.cascade_iterator("delete", child) 

747 for c, m, st_, dct_ in t: 

748 uowcommit.register_object(st_, isdelete=True) 

749 

750 def presort_saves(self, uowcommit, states): 

751 for state in states: 

752 uowcommit.register_object(state, operation="add", prop=self.prop) 

753 if self.cascade.delete_orphan: 

754 history = uowcommit.get_attribute_history( 

755 state, self.key, self._passive_delete_flag 

756 ) 

757 if history: 

758 for child in history.deleted: 

759 if self.hasparent(child) is False: 

760 uowcommit.register_object( 

761 child, 

762 isdelete=True, 

763 operation="delete", 

764 prop=self.prop, 

765 ) 

766 

767 t = self.mapper.cascade_iterator("delete", child) 

768 for c, m, st_, dct_ in t: 

769 uowcommit.register_object(st_, isdelete=True) 

770 

771 def process_deletes(self, uowcommit, states): 

772 if ( 

773 self.post_update 

774 and not self.cascade.delete_orphan 

775 and not self.passive_deletes == "all" 

776 ): 

777 

778 # post_update means we have to update our 

779 # row to not reference the child object 

780 # before we can DELETE the row 

781 for state in states: 

782 self._synchronize(state, None, None, True, uowcommit) 

783 if state and self.post_update: 

784 history = uowcommit.get_attribute_history( 

785 state, self.key, self._passive_delete_flag 

786 ) 

787 if history: 

788 self._post_update( 

789 state, uowcommit, history.sum(), is_m2o_delete=True 

790 ) 

791 

792 def process_saves(self, uowcommit, states): 

793 for state in states: 

794 history = uowcommit.get_attribute_history( 

795 state, self.key, attributes.PASSIVE_NO_INITIALIZE 

796 ) 

797 if history: 

798 if history.added: 

799 for child in history.added: 

800 self._synchronize( 

801 state, child, None, False, uowcommit, "add" 

802 ) 

803 elif history.deleted: 

804 self._synchronize( 

805 state, None, None, True, uowcommit, "delete" 

806 ) 

807 if self.post_update: 

808 self._post_update(state, uowcommit, history.sum()) 

809 

810 def _synchronize( 

811 self, 

812 state, 

813 child, 

814 associationrow, 

815 clearkeys, 

816 uowcommit, 

817 operation=None, 

818 ): 

819 if state is None or ( 

820 not self.post_update and uowcommit.is_deleted(state) 

821 ): 

822 return 

823 

824 if ( 

825 operation is not None 

826 and child is not None 

827 and not uowcommit.session._contains_state(child) 

828 ): 

829 util.warn( 

830 "Object of type %s not in session, %s " 

831 "operation along '%s' won't proceed" 

832 % (mapperutil.state_class_str(child), operation, self.prop) 

833 ) 

834 return 

835 

836 if clearkeys or child is None: 

837 sync.clear(state, self.parent, self.prop.synchronize_pairs) 

838 else: 

839 self._verify_canload(child) 

840 sync.populate( 

841 child, 

842 self.mapper, 

843 state, 

844 self.parent, 

845 self.prop.synchronize_pairs, 

846 uowcommit, 

847 False, 

848 ) 

849 

850 

851class DetectKeySwitch(DependencyProcessor): 

852 """For many-to-one relationships with no one-to-many backref, 

853 searches for parents through the unit of work when a primary 

854 key has changed and updates them. 

855 

856 Theoretically, this approach could be expanded to support transparent 

857 deletion of objects referenced via many-to-one as well, although 

858 the current attribute system doesn't do enough bookkeeping for this 

859 to be efficient. 

860 

861 """ 

862 

863 def per_property_preprocessors(self, uow): 

864 if self.prop._reverse_property: 

865 if self.passive_updates: 

866 return 

867 else: 

868 if False in ( 

869 prop.passive_updates 

870 for prop in self.prop._reverse_property 

871 ): 

872 return 

873 

874 uow.register_preprocessor(self, False) 

875 

876 def per_property_flush_actions(self, uow): 

877 parent_saves = unitofwork.SaveUpdateAll(uow, self.parent.base_mapper) 

878 after_save = unitofwork.ProcessAll(uow, self, False, False) 

879 uow.dependencies.update([(parent_saves, after_save)]) 

880 

881 def per_state_flush_actions(self, uow, states, isdelete): 

882 pass 

883 

884 def presort_deletes(self, uowcommit, states): 

885 pass 

886 

887 def presort_saves(self, uow, states): 

888 if not self.passive_updates: 

889 # for non-passive updates, register in the preprocess stage 

890 # so that mapper save_obj() gets a hold of changes 

891 self._process_key_switches(states, uow) 

892 

893 def prop_has_changes(self, uow, states, isdelete): 

894 if not isdelete and self.passive_updates: 

895 d = self._key_switchers(uow, states) 

896 return bool(d) 

897 

898 return False 

899 

900 def process_deletes(self, uowcommit, states): 

901 assert False 

902 

903 def process_saves(self, uowcommit, states): 

904 # for passive updates, register objects in the process stage 

905 # so that we avoid ManyToOneDP's registering the object without 

906 # the listonly flag in its own preprocess stage (results in UPDATE) 

907 # statements being emitted 

908 assert self.passive_updates 

909 self._process_key_switches(states, uowcommit) 

910 

911 def _key_switchers(self, uow, states): 

912 switched, notswitched = uow.memo( 

913 ("pk_switchers", self), lambda: (set(), set()) 

914 ) 

915 

916 allstates = switched.union(notswitched) 

917 for s in states: 

918 if s not in allstates: 

919 if self._pks_changed(uow, s): 

920 switched.add(s) 

921 else: 

922 notswitched.add(s) 

923 return switched 

924 

925 def _process_key_switches(self, deplist, uowcommit): 

926 switchers = self._key_switchers(uowcommit, deplist) 

927 if switchers: 

928 # if primary key values have actually changed somewhere, perform 

929 # a linear search through the UOW in search of a parent. 

930 for state in uowcommit.session.identity_map.all_states(): 

931 if not issubclass(state.class_, self.parent.class_): 

932 continue 

933 dict_ = state.dict 

934 related = state.get_impl(self.key).get( 

935 state, dict_, passive=self._passive_update_flag 

936 ) 

937 if ( 

938 related is not attributes.PASSIVE_NO_RESULT 

939 and related is not None 

940 ): 

941 if self.prop.uselist: 

942 if not related: 

943 continue 

944 related_obj = related[0] 

945 else: 

946 related_obj = related 

947 related_state = attributes.instance_state(related_obj) 

948 if related_state in switchers: 

949 uowcommit.register_object( 

950 state, False, self.passive_updates 

951 ) 

952 sync.populate( 

953 related_state, 

954 self.mapper, 

955 state, 

956 self.parent, 

957 self.prop.synchronize_pairs, 

958 uowcommit, 

959 self.passive_updates, 

960 ) 

961 

962 def _pks_changed(self, uowcommit, state): 

963 return bool(state.key) and sync.source_modified( 

964 uowcommit, state, self.mapper, self.prop.synchronize_pairs 

965 ) 

966 

967 

968class ManyToManyDP(DependencyProcessor): 

969 def per_property_dependencies( 

970 self, 

971 uow, 

972 parent_saves, 

973 child_saves, 

974 parent_deletes, 

975 child_deletes, 

976 after_save, 

977 before_delete, 

978 ): 

979 

980 uow.dependencies.update( 

981 [ 

982 (parent_saves, after_save), 

983 (child_saves, after_save), 

984 (after_save, child_deletes), 

985 # a rowswitch on the parent from deleted to saved 

986 # can make this one occur, as the "save" may remove 

987 # an element from the 

988 # "deleted" list before we have a chance to 

989 # process its child rows 

990 (before_delete, parent_saves), 

991 (before_delete, parent_deletes), 

992 (before_delete, child_deletes), 

993 (before_delete, child_saves), 

994 ] 

995 ) 

996 

997 def per_state_dependencies( 

998 self, 

999 uow, 

1000 save_parent, 

1001 delete_parent, 

1002 child_action, 

1003 after_save, 

1004 before_delete, 

1005 isdelete, 

1006 childisdelete, 

1007 ): 

1008 if not isdelete: 

1009 if childisdelete: 

1010 uow.dependencies.update( 

1011 [(save_parent, after_save), (after_save, child_action)] 

1012 ) 

1013 else: 

1014 uow.dependencies.update( 

1015 [(save_parent, after_save), (child_action, after_save)] 

1016 ) 

1017 else: 

1018 uow.dependencies.update( 

1019 [(before_delete, child_action), (before_delete, delete_parent)] 

1020 ) 

1021 

1022 def presort_deletes(self, uowcommit, states): 

1023 # TODO: no tests fail if this whole 

1024 # thing is removed !!!! 

1025 if not self.passive_deletes: 

1026 # if no passive deletes, load history on 

1027 # the collection, so that prop_has_changes() 

1028 # returns True 

1029 for state in states: 

1030 uowcommit.get_attribute_history( 

1031 state, self.key, self._passive_delete_flag 

1032 ) 

1033 

1034 def presort_saves(self, uowcommit, states): 

1035 if not self.passive_updates: 

1036 # if no passive updates, load history on 

1037 # each collection where parent has changed PK, 

1038 # so that prop_has_changes() returns True 

1039 for state in states: 

1040 if self._pks_changed(uowcommit, state): 

1041 history = uowcommit.get_attribute_history( 

1042 state, self.key, attributes.PASSIVE_OFF 

1043 ) 

1044 

1045 if not self.cascade.delete_orphan: 

1046 return 

1047 

1048 # check for child items removed from the collection 

1049 # if delete_orphan check is turned on. 

1050 for state in states: 

1051 history = uowcommit.get_attribute_history( 

1052 state, self.key, attributes.PASSIVE_NO_INITIALIZE 

1053 ) 

1054 if history: 

1055 for child in history.deleted: 

1056 if self.hasparent(child) is False: 

1057 uowcommit.register_object( 

1058 child, 

1059 isdelete=True, 

1060 operation="delete", 

1061 prop=self.prop, 

1062 ) 

1063 for c, m, st_, dct_ in self.mapper.cascade_iterator( 

1064 "delete", child 

1065 ): 

1066 uowcommit.register_object(st_, isdelete=True) 

1067 

1068 def process_deletes(self, uowcommit, states): 

1069 secondary_delete = [] 

1070 secondary_insert = [] 

1071 secondary_update = [] 

1072 

1073 processed = self._get_reversed_processed_set(uowcommit) 

1074 tmp = set() 

1075 for state in states: 

1076 # this history should be cached already, as 

1077 # we loaded it in preprocess_deletes 

1078 history = uowcommit.get_attribute_history( 

1079 state, self.key, self._passive_delete_flag 

1080 ) 

1081 if history: 

1082 for child in history.non_added(): 

1083 if child is None or ( 

1084 processed is not None and (state, child) in processed 

1085 ): 

1086 continue 

1087 associationrow = {} 

1088 if not self._synchronize( 

1089 state, 

1090 child, 

1091 associationrow, 

1092 False, 

1093 uowcommit, 

1094 "delete", 

1095 ): 

1096 continue 

1097 secondary_delete.append(associationrow) 

1098 

1099 tmp.update((c, state) for c in history.non_added()) 

1100 

1101 if processed is not None: 

1102 processed.update(tmp) 

1103 

1104 self._run_crud( 

1105 uowcommit, secondary_insert, secondary_update, secondary_delete 

1106 ) 

1107 

1108 def process_saves(self, uowcommit, states): 

1109 secondary_delete = [] 

1110 secondary_insert = [] 

1111 secondary_update = [] 

1112 

1113 processed = self._get_reversed_processed_set(uowcommit) 

1114 tmp = set() 

1115 

1116 for state in states: 

1117 need_cascade_pks = not self.passive_updates and self._pks_changed( 

1118 uowcommit, state 

1119 ) 

1120 if need_cascade_pks: 

1121 passive = attributes.PASSIVE_OFF 

1122 else: 

1123 passive = attributes.PASSIVE_NO_INITIALIZE 

1124 history = uowcommit.get_attribute_history(state, self.key, passive) 

1125 if history: 

1126 for child in history.added: 

1127 if processed is not None and (state, child) in processed: 

1128 continue 

1129 associationrow = {} 

1130 if not self._synchronize( 

1131 state, child, associationrow, False, uowcommit, "add" 

1132 ): 

1133 continue 

1134 secondary_insert.append(associationrow) 

1135 for child in history.deleted: 

1136 if processed is not None and (state, child) in processed: 

1137 continue 

1138 associationrow = {} 

1139 if not self._synchronize( 

1140 state, 

1141 child, 

1142 associationrow, 

1143 False, 

1144 uowcommit, 

1145 "delete", 

1146 ): 

1147 continue 

1148 secondary_delete.append(associationrow) 

1149 

1150 tmp.update((c, state) for c in history.added + history.deleted) 

1151 

1152 if need_cascade_pks: 

1153 

1154 for child in history.unchanged: 

1155 associationrow = {} 

1156 sync.update( 

1157 state, 

1158 self.parent, 

1159 associationrow, 

1160 "old_", 

1161 self.prop.synchronize_pairs, 

1162 ) 

1163 sync.update( 

1164 child, 

1165 self.mapper, 

1166 associationrow, 

1167 "old_", 

1168 self.prop.secondary_synchronize_pairs, 

1169 ) 

1170 

1171 secondary_update.append(associationrow) 

1172 

1173 if processed is not None: 

1174 processed.update(tmp) 

1175 

1176 self._run_crud( 

1177 uowcommit, secondary_insert, secondary_update, secondary_delete 

1178 ) 

1179 

1180 def _run_crud( 

1181 self, uowcommit, secondary_insert, secondary_update, secondary_delete 

1182 ): 

1183 connection = uowcommit.transaction.connection(self.mapper) 

1184 

1185 if secondary_delete: 

1186 associationrow = secondary_delete[0] 

1187 statement = self.secondary.delete( 

1188 sql.and_( 

1189 *[ 

1190 c == sql.bindparam(c.key, type_=c.type) 

1191 for c in self.secondary.c 

1192 if c.key in associationrow 

1193 ] 

1194 ) 

1195 ) 

1196 result = connection.execute(statement, secondary_delete) 

1197 

1198 if ( 

1199 result.supports_sane_multi_rowcount() 

1200 ) and result.rowcount != len(secondary_delete): 

1201 raise exc.StaleDataError( 

1202 "DELETE statement on table '%s' expected to delete " 

1203 "%d row(s); Only %d were matched." 

1204 % ( 

1205 self.secondary.description, 

1206 len(secondary_delete), 

1207 result.rowcount, 

1208 ) 

1209 ) 

1210 

1211 if secondary_update: 

1212 associationrow = secondary_update[0] 

1213 statement = self.secondary.update( 

1214 sql.and_( 

1215 *[ 

1216 c == sql.bindparam("old_" + c.key, type_=c.type) 

1217 for c in self.secondary.c 

1218 if c.key in associationrow 

1219 ] 

1220 ) 

1221 ) 

1222 result = connection.execute(statement, secondary_update) 

1223 

1224 if ( 

1225 result.supports_sane_multi_rowcount() 

1226 ) and result.rowcount != len(secondary_update): 

1227 raise exc.StaleDataError( 

1228 "UPDATE statement on table '%s' expected to update " 

1229 "%d row(s); Only %d were matched." 

1230 % ( 

1231 self.secondary.description, 

1232 len(secondary_update), 

1233 result.rowcount, 

1234 ) 

1235 ) 

1236 

1237 if secondary_insert: 

1238 statement = self.secondary.insert() 

1239 connection.execute(statement, secondary_insert) 

1240 

1241 def _synchronize( 

1242 self, state, child, associationrow, clearkeys, uowcommit, operation 

1243 ): 

1244 

1245 # this checks for None if uselist=True 

1246 self._verify_canload(child) 

1247 

1248 # but if uselist=False we get here. If child is None, 

1249 # no association row can be generated, so return. 

1250 if child is None: 

1251 return False 

1252 

1253 if child is not None and not uowcommit.session._contains_state(child): 

1254 if not child.deleted: 

1255 util.warn( 

1256 "Object of type %s not in session, %s " 

1257 "operation along '%s' won't proceed" 

1258 % (mapperutil.state_class_str(child), operation, self.prop) 

1259 ) 

1260 return False 

1261 

1262 sync.populate_dict( 

1263 state, self.parent, associationrow, self.prop.synchronize_pairs 

1264 ) 

1265 sync.populate_dict( 

1266 child, 

1267 self.mapper, 

1268 associationrow, 

1269 self.prop.secondary_synchronize_pairs, 

1270 ) 

1271 

1272 return True 

1273 

1274 def _pks_changed(self, uowcommit, state): 

1275 return sync.source_modified( 

1276 uowcommit, state, self.parent, self.prop.synchronize_pairs 

1277 ) 

1278 

1279 

1280_direction_to_processor = { 

1281 ONETOMANY: OneToManyDP, 

1282 MANYTOONE: ManyToOneDP, 

1283 MANYTOMANY: ManyToManyDP, 

1284}