Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/django/db/models/deletion.py: 15%

243 statements  

« prev     ^ index     » next       coverage.py v7.0.5, created at 2023-01-17 06:13 +0000

1from collections import Counter, defaultdict 

2from functools import partial, reduce 

3from itertools import chain 

4from operator import attrgetter, or_ 

5 

6from django.db import IntegrityError, connections, models, transaction 

7from django.db.models import query_utils, signals, sql 

8 

9 

10class ProtectedError(IntegrityError): 

11 def __init__(self, msg, protected_objects): 

12 self.protected_objects = protected_objects 

13 super().__init__(msg, protected_objects) 

14 

15 

16class RestrictedError(IntegrityError): 

17 def __init__(self, msg, restricted_objects): 

18 self.restricted_objects = restricted_objects 

19 super().__init__(msg, restricted_objects) 

20 

21 

22def CASCADE(collector, field, sub_objs, using): 

23 collector.collect( 

24 sub_objs, 

25 source=field.remote_field.model, 

26 source_attr=field.name, 

27 nullable=field.null, 

28 fail_on_restricted=False, 

29 ) 

30 if field.null and not connections[using].features.can_defer_constraint_checks: 

31 collector.add_field_update(field, None, sub_objs) 

32 

33 

34def PROTECT(collector, field, sub_objs, using): 

35 raise ProtectedError( 

36 "Cannot delete some instances of model '%s' because they are " 

37 "referenced through a protected foreign key: '%s.%s'" 

38 % ( 

39 field.remote_field.model.__name__, 

40 sub_objs[0].__class__.__name__, 

41 field.name, 

42 ), 

43 sub_objs, 

44 ) 

45 

46 

47def RESTRICT(collector, field, sub_objs, using): 

48 collector.add_restricted_objects(field, sub_objs) 

49 collector.add_dependency(field.remote_field.model, field.model) 

50 

51 

52def SET(value): 

53 if callable(value): 

54 

55 def set_on_delete(collector, field, sub_objs, using): 

56 collector.add_field_update(field, value(), sub_objs) 

57 

58 else: 

59 

60 def set_on_delete(collector, field, sub_objs, using): 

61 collector.add_field_update(field, value, sub_objs) 

62 

63 set_on_delete.deconstruct = lambda: ("django.db.models.SET", (value,), {}) 

64 set_on_delete.lazy_sub_objs = True 

65 return set_on_delete 

66 

67 

68def SET_NULL(collector, field, sub_objs, using): 

69 collector.add_field_update(field, None, sub_objs) 

70 

71 

72SET_NULL.lazy_sub_objs = True 

73 

74 

75def SET_DEFAULT(collector, field, sub_objs, using): 

76 collector.add_field_update(field, field.get_default(), sub_objs) 

77 

78 

79SET_DEFAULT.lazy_sub_objs = True 

80 

81 

82def DO_NOTHING(collector, field, sub_objs, using): 

83 pass 

84 

85 

86def get_candidate_relations_to_delete(opts): 

87 # The candidate relations are the ones that come from N-1 and 1-1 relations. 

88 # N-N (i.e., many-to-many) relations aren't candidates for deletion. 

89 return ( 

90 f 

91 for f in opts.get_fields(include_hidden=True) 

92 if f.auto_created and not f.concrete and (f.one_to_one or f.one_to_many) 

93 ) 

94 

95 

96class Collector: 

97 def __init__(self, using, origin=None): 

98 self.using = using 

99 # A Model or QuerySet object. 

100 self.origin = origin 

101 # Initially, {model: {instances}}, later values become lists. 

102 self.data = defaultdict(set) 

103 # {(field, value): [instances, …]} 

104 self.field_updates = defaultdict(list) 

105 # {model: {field: {instances}}} 

106 self.restricted_objects = defaultdict(partial(defaultdict, set)) 

107 # fast_deletes is a list of queryset-likes that can be deleted without 

108 # fetching the objects into memory. 

109 self.fast_deletes = [] 

110 

111 # Tracks deletion-order dependency for databases without transactions 

112 # or ability to defer constraint checks. Only concrete model classes 

113 # should be included, as the dependencies exist only between actual 

114 # database tables; proxy models are represented here by their concrete 

115 # parent. 

116 self.dependencies = defaultdict(set) # {model: {models}} 

117 

118 def add(self, objs, source=None, nullable=False, reverse_dependency=False): 

119 """ 

120 Add 'objs' to the collection of objects to be deleted. If the call is 

121 the result of a cascade, 'source' should be the model that caused it, 

122 and 'nullable' should be set to True if the relation can be null. 

123 

124 Return a list of all objects that were not already collected. 

125 """ 

126 if not objs: 

127 return [] 

128 new_objs = [] 

129 model = objs[0].__class__ 

130 instances = self.data[model] 

131 for obj in objs: 

132 if obj not in instances: 

133 new_objs.append(obj) 

134 instances.update(new_objs) 

135 # Nullable relationships can be ignored -- they are nulled out before 

136 # deleting, and therefore do not affect the order in which objects have 

137 # to be deleted. 

138 if source is not None and not nullable: 

139 self.add_dependency(source, model, reverse_dependency=reverse_dependency) 

140 return new_objs 

141 

142 def add_dependency(self, model, dependency, reverse_dependency=False): 

143 if reverse_dependency: 

144 model, dependency = dependency, model 

145 self.dependencies[model._meta.concrete_model].add( 

146 dependency._meta.concrete_model 

147 ) 

148 self.data.setdefault(dependency, self.data.default_factory()) 

149 

150 def add_field_update(self, field, value, objs): 

151 """ 

152 Schedule a field update. 'objs' must be a homogeneous iterable 

153 collection of model instances (e.g. a QuerySet). 

154 """ 

155 self.field_updates[field, value].append(objs) 

156 

157 def add_restricted_objects(self, field, objs): 

158 if objs: 

159 model = objs[0].__class__ 

160 self.restricted_objects[model][field].update(objs) 

161 

162 def clear_restricted_objects_from_set(self, model, objs): 

163 if model in self.restricted_objects: 

164 self.restricted_objects[model] = { 

165 field: items - objs 

166 for field, items in self.restricted_objects[model].items() 

167 } 

168 

169 def clear_restricted_objects_from_queryset(self, model, qs): 

170 if model in self.restricted_objects: 

171 objs = set( 

172 qs.filter( 

173 pk__in=[ 

174 obj.pk 

175 for objs in self.restricted_objects[model].values() 

176 for obj in objs 

177 ] 

178 ) 

179 ) 

180 self.clear_restricted_objects_from_set(model, objs) 

181 

182 def _has_signal_listeners(self, model): 

183 return signals.pre_delete.has_listeners( 

184 model 

185 ) or signals.post_delete.has_listeners(model) 

186 

187 def can_fast_delete(self, objs, from_field=None): 

188 """ 

189 Determine if the objects in the given queryset-like or single object 

190 can be fast-deleted. This can be done if there are no cascades, no 

191 parents and no signal listeners for the object class. 

192 

193 The 'from_field' tells where we are coming from - we need this to 

194 determine if the objects are in fact to be deleted. Allow also 

195 skipping parent -> child -> parent chain preventing fast delete of 

196 the child. 

197 """ 

198 if from_field and from_field.remote_field.on_delete is not CASCADE: 

199 return False 

200 if hasattr(objs, "_meta"): 

201 model = objs._meta.model 

202 elif hasattr(objs, "model") and hasattr(objs, "_raw_delete"): 

203 model = objs.model 

204 else: 

205 return False 

206 if self._has_signal_listeners(model): 

207 return False 

208 # The use of from_field comes from the need to avoid cascade back to 

209 # parent when parent delete is cascading to child. 

210 opts = model._meta 

211 return ( 

212 all( 

213 link == from_field 

214 for link in opts.concrete_model._meta.parents.values() 

215 ) 

216 and 

217 # Foreign keys pointing to this model. 

218 all( 

219 related.field.remote_field.on_delete is DO_NOTHING 

220 for related in get_candidate_relations_to_delete(opts) 

221 ) 

222 and ( 

223 # Something like generic foreign key. 

224 not any( 

225 hasattr(field, "bulk_related_objects") 

226 for field in opts.private_fields 

227 ) 

228 ) 

229 ) 

230 

231 def get_del_batches(self, objs, fields): 

232 """ 

233 Return the objs in suitably sized batches for the used connection. 

234 """ 

235 field_names = [field.name for field in fields] 

236 conn_batch_size = max( 

237 connections[self.using].ops.bulk_batch_size(field_names, objs), 1 

238 ) 

239 if len(objs) > conn_batch_size: 

240 return [ 

241 objs[i : i + conn_batch_size] 

242 for i in range(0, len(objs), conn_batch_size) 

243 ] 

244 else: 

245 return [objs] 

246 

247 def collect( 

248 self, 

249 objs, 

250 source=None, 

251 nullable=False, 

252 collect_related=True, 

253 source_attr=None, 

254 reverse_dependency=False, 

255 keep_parents=False, 

256 fail_on_restricted=True, 

257 ): 

258 """ 

259 Add 'objs' to the collection of objects to be deleted as well as all 

260 parent instances. 'objs' must be a homogeneous iterable collection of 

261 model instances (e.g. a QuerySet). If 'collect_related' is True, 

262 related objects will be handled by their respective on_delete handler. 

263 

264 If the call is the result of a cascade, 'source' should be the model 

265 that caused it and 'nullable' should be set to True, if the relation 

266 can be null. 

267 

268 If 'reverse_dependency' is True, 'source' will be deleted before the 

269 current model, rather than after. (Needed for cascading to parent 

270 models, the one case in which the cascade follows the forwards 

271 direction of an FK rather than the reverse direction.) 

272 

273 If 'keep_parents' is True, data of parent model's will be not deleted. 

274 

275 If 'fail_on_restricted' is False, error won't be raised even if it's 

276 prohibited to delete such objects due to RESTRICT, that defers 

277 restricted object checking in recursive calls where the top-level call 

278 may need to collect more objects to determine whether restricted ones 

279 can be deleted. 

280 """ 

281 if self.can_fast_delete(objs): 

282 self.fast_deletes.append(objs) 

283 return 

284 new_objs = self.add( 

285 objs, source, nullable, reverse_dependency=reverse_dependency 

286 ) 

287 if not new_objs: 

288 return 

289 

290 model = new_objs[0].__class__ 

291 

292 if not keep_parents: 

293 # Recursively collect concrete model's parent models, but not their 

294 # related objects. These will be found by meta.get_fields() 

295 concrete_model = model._meta.concrete_model 

296 for ptr in concrete_model._meta.parents.values(): 

297 if ptr: 

298 parent_objs = [getattr(obj, ptr.name) for obj in new_objs] 

299 self.collect( 

300 parent_objs, 

301 source=model, 

302 source_attr=ptr.remote_field.related_name, 

303 collect_related=False, 

304 reverse_dependency=True, 

305 fail_on_restricted=False, 

306 ) 

307 if not collect_related: 

308 return 

309 

310 if keep_parents: 

311 parents = set(model._meta.get_parent_list()) 

312 model_fast_deletes = defaultdict(list) 

313 protected_objects = defaultdict(list) 

314 for related in get_candidate_relations_to_delete(model._meta): 

315 # Preserve parent reverse relationships if keep_parents=True. 

316 if keep_parents and related.model in parents: 

317 continue 

318 field = related.field 

319 on_delete = field.remote_field.on_delete 

320 if on_delete == DO_NOTHING: 

321 continue 

322 related_model = related.related_model 

323 if self.can_fast_delete(related_model, from_field=field): 

324 model_fast_deletes[related_model].append(field) 

325 continue 

326 batches = self.get_del_batches(new_objs, [field]) 

327 for batch in batches: 

328 sub_objs = self.related_objects(related_model, [field], batch) 

329 # Non-referenced fields can be deferred if no signal receivers 

330 # are connected for the related model as they'll never be 

331 # exposed to the user. Skip field deferring when some 

332 # relationships are select_related as interactions between both 

333 # features are hard to get right. This should only happen in 

334 # the rare cases where .related_objects is overridden anyway. 

335 if not ( 

336 sub_objs.query.select_related 

337 or self._has_signal_listeners(related_model) 

338 ): 

339 referenced_fields = set( 

340 chain.from_iterable( 

341 (rf.attname for rf in rel.field.foreign_related_fields) 

342 for rel in get_candidate_relations_to_delete( 

343 related_model._meta 

344 ) 

345 ) 

346 ) 

347 sub_objs = sub_objs.only(*tuple(referenced_fields)) 

348 if getattr(on_delete, "lazy_sub_objs", False) or sub_objs: 

349 try: 

350 on_delete(self, field, sub_objs, self.using) 

351 except ProtectedError as error: 

352 key = "'%s.%s'" % (field.model.__name__, field.name) 

353 protected_objects[key] += error.protected_objects 

354 if protected_objects: 

355 raise ProtectedError( 

356 "Cannot delete some instances of model %r because they are " 

357 "referenced through protected foreign keys: %s." 

358 % ( 

359 model.__name__, 

360 ", ".join(protected_objects), 

361 ), 

362 set(chain.from_iterable(protected_objects.values())), 

363 ) 

364 for related_model, related_fields in model_fast_deletes.items(): 

365 batches = self.get_del_batches(new_objs, related_fields) 

366 for batch in batches: 

367 sub_objs = self.related_objects(related_model, related_fields, batch) 

368 self.fast_deletes.append(sub_objs) 

369 for field in model._meta.private_fields: 

370 if hasattr(field, "bulk_related_objects"): 

371 # It's something like generic foreign key. 

372 sub_objs = field.bulk_related_objects(new_objs, self.using) 

373 self.collect( 

374 sub_objs, source=model, nullable=True, fail_on_restricted=False 

375 ) 

376 

377 if fail_on_restricted: 

378 # Raise an error if collected restricted objects (RESTRICT) aren't 

379 # candidates for deletion also collected via CASCADE. 

380 for related_model, instances in self.data.items(): 

381 self.clear_restricted_objects_from_set(related_model, instances) 

382 for qs in self.fast_deletes: 

383 self.clear_restricted_objects_from_queryset(qs.model, qs) 

384 if self.restricted_objects.values(): 

385 restricted_objects = defaultdict(list) 

386 for related_model, fields in self.restricted_objects.items(): 

387 for field, objs in fields.items(): 

388 if objs: 

389 key = "'%s.%s'" % (related_model.__name__, field.name) 

390 restricted_objects[key] += objs 

391 if restricted_objects: 

392 raise RestrictedError( 

393 "Cannot delete some instances of model %r because " 

394 "they are referenced through restricted foreign keys: " 

395 "%s." 

396 % ( 

397 model.__name__, 

398 ", ".join(restricted_objects), 

399 ), 

400 set(chain.from_iterable(restricted_objects.values())), 

401 ) 

402 

403 def related_objects(self, related_model, related_fields, objs): 

404 """ 

405 Get a QuerySet of the related model to objs via related fields. 

406 """ 

407 predicate = query_utils.Q.create( 

408 [(f"{related_field.name}__in", objs) for related_field in related_fields], 

409 connector=query_utils.Q.OR, 

410 ) 

411 return related_model._base_manager.using(self.using).filter(predicate) 

412 

413 def instances_with_model(self): 

414 for model, instances in self.data.items(): 

415 for obj in instances: 

416 yield model, obj 

417 

418 def sort(self): 

419 sorted_models = [] 

420 concrete_models = set() 

421 models = list(self.data) 

422 while len(sorted_models) < len(models): 

423 found = False 

424 for model in models: 

425 if model in sorted_models: 

426 continue 

427 dependencies = self.dependencies.get(model._meta.concrete_model) 

428 if not (dependencies and dependencies.difference(concrete_models)): 

429 sorted_models.append(model) 

430 concrete_models.add(model._meta.concrete_model) 

431 found = True 

432 if not found: 

433 return 

434 self.data = {model: self.data[model] for model in sorted_models} 

435 

436 def delete(self): 

437 # sort instance collections 

438 for model, instances in self.data.items(): 

439 self.data[model] = sorted(instances, key=attrgetter("pk")) 

440 

441 # if possible, bring the models in an order suitable for databases that 

442 # don't support transactions or cannot defer constraint checks until the 

443 # end of a transaction. 

444 self.sort() 

445 # number of objects deleted for each model label 

446 deleted_counter = Counter() 

447 

448 # Optimize for the case with a single obj and no dependencies 

449 if len(self.data) == 1 and len(instances) == 1: 

450 instance = list(instances)[0] 

451 if self.can_fast_delete(instance): 

452 with transaction.mark_for_rollback_on_error(self.using): 

453 count = sql.DeleteQuery(model).delete_batch( 

454 [instance.pk], self.using 

455 ) 

456 setattr(instance, model._meta.pk.attname, None) 

457 return count, {model._meta.label: count} 

458 

459 with transaction.atomic(using=self.using, savepoint=False): 

460 # send pre_delete signals 

461 for model, obj in self.instances_with_model(): 

462 if not model._meta.auto_created: 

463 signals.pre_delete.send( 

464 sender=model, 

465 instance=obj, 

466 using=self.using, 

467 origin=self.origin, 

468 ) 

469 

470 # fast deletes 

471 for qs in self.fast_deletes: 

472 count = qs._raw_delete(using=self.using) 

473 if count: 

474 deleted_counter[qs.model._meta.label] += count 

475 

476 # update fields 

477 for (field, value), instances_list in self.field_updates.items(): 

478 updates = [] 

479 objs = [] 

480 for instances in instances_list: 

481 if ( 

482 isinstance(instances, models.QuerySet) 

483 and instances._result_cache is None 

484 ): 

485 updates.append(instances) 

486 else: 

487 objs.extend(instances) 

488 if updates: 

489 combined_updates = reduce(or_, updates) 

490 combined_updates.update(**{field.name: value}) 

491 if objs: 

492 model = objs[0].__class__ 

493 query = sql.UpdateQuery(model) 

494 query.update_batch( 

495 list({obj.pk for obj in objs}), {field.name: value}, self.using 

496 ) 

497 

498 # reverse instance collections 

499 for instances in self.data.values(): 

500 instances.reverse() 

501 

502 # delete instances 

503 for model, instances in self.data.items(): 

504 query = sql.DeleteQuery(model) 

505 pk_list = [obj.pk for obj in instances] 

506 count = query.delete_batch(pk_list, self.using) 

507 if count: 

508 deleted_counter[model._meta.label] += count 

509 

510 if not model._meta.auto_created: 

511 for obj in instances: 

512 signals.post_delete.send( 

513 sender=model, 

514 instance=obj, 

515 using=self.using, 

516 origin=self.origin, 

517 ) 

518 

519 for model, instances in self.data.items(): 

520 for instance in instances: 

521 setattr(instance, model._meta.pk.attname, None) 

522 return sum(deleted_counter.values()), dict(deleted_counter)