django-bulk-hooks 0.1.125__py3-none-any.whl → 0.1.127__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of django-bulk-hooks might be problematic. Click here for more details.

@@ -1,4 +1,3 @@
1
-
2
1
  from django.db import models, transaction
3
2
  from django.db.models import AutoField
4
3
 
@@ -44,7 +43,7 @@ class HookQuerySet(models.QuerySet):
44
43
  for obj in instances:
45
44
  for field, value in kwargs.items():
46
45
  setattr(obj, field, value)
47
-
46
+
48
47
  # Run BEFORE_UPDATE hooks
49
48
  ctx = HookContext(model_cls)
50
49
  engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
@@ -77,10 +76,26 @@ class HookQuerySet(models.QuerySet):
77
76
  """
78
77
  model_cls = self.model
79
78
 
79
+ # When you bulk insert you don't get the primary keys back (if it's an
80
+ # autoincrement, except if can_return_rows_from_bulk_insert=True), so
81
+ # you can't insert into the child tables which references this. There
82
+ # are two workarounds:
83
+ # 1) This could be implemented if you didn't have an autoincrement pk
84
+ # 2) You could do it by doing O(n) normal inserts into the parent
85
+ # tables to get the primary keys back and then doing a single bulk
86
+ # insert into the childmost table.
87
+ # We currently set the primary keys on the objects when using
88
+ # PostgreSQL via the RETURNING ID clause. It should be possible for
89
+ # Oracle as well, but the semantics for extracting the primary keys is
90
+ # trickier so it's not done yet.
80
91
  if batch_size is not None and batch_size <= 0:
81
92
  raise ValueError("Batch size must be a positive integer.")
82
93
 
83
94
  # Check for MTI - if we detect multi-table inheritance, we need special handling
95
+ # This follows Django's approach: check that the parents share the same concrete model
96
+ # with our model to detect the inheritance pattern ConcreteGrandParent ->
97
+ # MultiTableParent -> ProxyChild. Simply checking self.model._meta.proxy would not
98
+ # identify that case as involving multiple tables.
84
99
  is_mti = False
85
100
  for parent in model_cls._meta.all_parents:
86
101
  if parent._meta.concrete_model is not model_cls._meta.concrete_model:
@@ -116,7 +131,7 @@ class HookQuerySet(models.QuerySet):
116
131
  else:
117
132
  # For single-table models, use Django's built-in bulk_create
118
133
  # but we need to call it on the base manager to avoid recursion
119
-
134
+
120
135
  result = model_cls._base_manager.bulk_create(
121
136
  objs,
122
137
  batch_size=batch_size,
@@ -172,7 +187,7 @@ class HookQuerySet(models.QuerySet):
172
187
 
173
188
  for i in range(0, len(objs), self.CHUNK_SIZE):
174
189
  chunk = objs[i : i + self.CHUNK_SIZE]
175
-
190
+
176
191
  # Call the base implementation to avoid re-triggering this method
177
192
  super().bulk_update(chunk, fields, **kwargs)
178
193
 
@@ -182,7 +197,9 @@ class HookQuerySet(models.QuerySet):
182
197
  return objs
183
198
 
184
199
  @transaction.atomic
185
- def bulk_delete(self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False):
200
+ def bulk_delete(
201
+ self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
202
+ ):
186
203
  if not objs:
187
204
  return []
188
205
 
@@ -214,8 +231,6 @@ class HookQuerySet(models.QuerySet):
214
231
 
215
232
  return objs
216
233
 
217
- # --- Private helper methods ---
218
-
219
234
  def _detect_modified_fields(self, new_instances, original_instances):
220
235
  """
221
236
  Detect fields that were modified during BEFORE_UPDATE hooks by comparing
@@ -274,16 +289,20 @@ class HookQuerySet(models.QuerySet):
274
289
 
275
290
  def _mti_bulk_create(self, objs, inheritance_chain=None, **kwargs):
276
291
  """
277
- Implements workaround: individual saves for parents, bulk create for child.
292
+ Implements Django's suggested workaround #2 for MTI bulk_create:
293
+ O(n) normal inserts into parent tables to get primary keys back,
294
+ then single bulk insert into childmost table.
278
295
  Sets auto_now_add/auto_now fields for each model in the chain.
279
296
  """
280
297
  if inheritance_chain is None:
281
298
  inheritance_chain = self._get_inheritance_chain()
282
-
299
+
283
300
  # Safety check to prevent infinite recursion
284
301
  if len(inheritance_chain) > 10: # Arbitrary limit to prevent infinite loops
285
- raise ValueError("Inheritance chain too deep - possible infinite recursion detected")
286
-
302
+ raise ValueError(
303
+ "Inheritance chain too deep - possible infinite recursion detected"
304
+ )
305
+
287
306
  batch_size = kwargs.get("batch_size") or len(objs)
288
307
  created_objects = []
289
308
  with transaction.atomic(using=self.db, savepoint=False):
@@ -298,13 +317,14 @@ class HookQuerySet(models.QuerySet):
298
317
  def _process_mti_batch(self, batch, inheritance_chain, **kwargs):
299
318
  """
300
319
  Process a single batch of objects through the inheritance chain.
301
- Reuses Django's internal functions as much as possible.
320
+ Implements Django's suggested workaround #2: O(n) normal inserts into parent
321
+ tables to get primary keys back, then single bulk insert into childmost table.
302
322
  """
303
323
  # For MTI, we need to save parent objects first to get PKs
304
324
  # Then we can use Django's bulk_create for the child objects
305
325
  parent_objects_map = {}
306
-
307
- # Step 1: Save parent objects (we need their PKs for child objects)
326
+
327
+ # Step 1: Do O(n) normal inserts into parent tables to get primary keys back
308
328
  for obj in batch:
309
329
  parent_instances = {}
310
330
  current_parent = None
@@ -312,33 +332,34 @@ class HookQuerySet(models.QuerySet):
312
332
  parent_obj = self._create_parent_instance(
313
333
  obj, model_class, current_parent
314
334
  )
315
- # Use Django's internal save method to avoid hooks
316
- models.Model.save(parent_obj)
335
+ # Use Django's internal _insert method to get PKs back
336
+ # This bypasses hooks and the MTI exception
337
+ parent_obj._do_insert(parent_obj._meta, using=self.db)
317
338
  parent_instances[model_class] = parent_obj
318
339
  current_parent = parent_obj
319
340
  parent_objects_map[id(obj)] = parent_instances
320
-
321
- # Step 2: Create and bulk insert child objects
341
+
342
+ # Step 2: Create all child objects and do single bulk insert into childmost table
322
343
  child_model = inheritance_chain[-1]
323
- child_objects = []
344
+ all_child_objects = []
324
345
  for obj in batch:
325
346
  child_obj = self._create_child_instance(
326
347
  obj, child_model, parent_objects_map.get(id(obj), {})
327
348
  )
328
- child_objects.append(child_obj)
329
-
330
- # Use Django's bulk_create for child objects - this handles auto_now_add correctly
331
- child_manager = child_model._base_manager
332
- child_manager._for_write = True
333
- created = child_manager.bulk_create(child_objects, **kwargs)
334
-
349
+ all_child_objects.append(child_obj)
350
+
351
+ # Step 2.5: Single bulk insert into childmost table
352
+ if all_child_objects:
353
+ # Use Django's internal bulk_create to bypass MTI exception
354
+ child_model._base_manager.bulk_create(all_child_objects)
355
+
335
356
  # Step 3: Update original objects with generated PKs and state
336
357
  pk_field_name = child_model._meta.pk.name
337
- for orig_obj, child_obj in zip(batch, created):
358
+ for orig_obj, child_obj in zip(batch, all_child_objects):
338
359
  setattr(orig_obj, pk_field_name, getattr(child_obj, pk_field_name))
339
360
  orig_obj._state.adding = False
340
361
  orig_obj._state.db = self.db
341
-
362
+
342
363
  return batch
343
364
 
344
365
  def _create_parent_instance(self, source_obj, parent_model, current_parent):
@@ -358,20 +379,18 @@ class HookQuerySet(models.QuerySet):
358
379
  ):
359
380
  setattr(parent_obj, field.name, current_parent)
360
381
  break
361
-
382
+
362
383
  # Handle auto_now_add and auto_now fields like Django does
363
384
  for field in parent_model._meta.local_fields:
364
- if hasattr(field, 'auto_now_add') and field.auto_now_add:
365
- field.pre_save(parent_obj, add=True)
366
- elif hasattr(field, 'auto_now') and field.auto_now:
367
- field.pre_save(parent_obj, add=True)
368
-
369
- # Ensure auto_now_add fields are explicitly set to prevent null constraint violations
370
- for field in parent_model._meta.local_fields:
371
- if hasattr(field, 'auto_now_add') and field.auto_now_add:
385
+ if hasattr(field, "auto_now_add") and field.auto_now_add:
386
+ # Ensure auto_now_add fields are properly set
372
387
  if getattr(parent_obj, field.name) is None:
373
388
  field.pre_save(parent_obj, add=True)
374
-
389
+ # Explicitly set the value to ensure it's not None
390
+ setattr(parent_obj, field.name, field.value_from_object(parent_obj))
391
+ elif hasattr(field, "auto_now") and field.auto_now:
392
+ field.pre_save(parent_obj, add=True)
393
+
375
394
  return parent_obj
376
395
 
377
396
  def _create_child_instance(self, source_obj, child_model, parent_instances):
@@ -387,19 +406,16 @@ class HookQuerySet(models.QuerySet):
387
406
  parent_link = child_model._meta.get_ancestor_link(parent_model)
388
407
  if parent_link:
389
408
  setattr(child_obj, parent_link.name, parent_instance)
390
-
409
+
391
410
  # Handle auto_now_add and auto_now fields like Django does
392
411
  for field in child_model._meta.local_fields:
393
- if hasattr(field, 'auto_now_add') and field.auto_now_add:
394
- field.pre_save(child_obj, add=True)
395
- elif hasattr(field, 'auto_now') and field.auto_now:
396
- field.pre_save(child_obj, add=True)
397
-
398
- # Ensure auto_now_add fields are explicitly set to prevent null constraint violations
399
- for field in child_model._meta.local_fields:
400
- if hasattr(field, 'auto_now_add') and field.auto_now_add:
412
+ if hasattr(field, "auto_now_add") and field.auto_now_add:
413
+ # Ensure auto_now_add fields are properly set
401
414
  if getattr(child_obj, field.name) is None:
402
415
  field.pre_save(child_obj, add=True)
403
-
404
- return child_obj
416
+ # Explicitly set the value to ensure it's not None
417
+ setattr(child_obj, field.name, field.value_from_object(child_obj))
418
+ elif hasattr(field, "auto_now") and field.auto_now:
419
+ field.pre_save(child_obj, add=True)
405
420
 
421
+ return child_obj
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.125
3
+ Version: 0.1.127
4
4
  Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
5
5
  License: MIT
6
6
  Keywords: django,bulk,hooks
@@ -9,9 +9,9 @@ django_bulk_hooks/handler.py,sha256=xZt8iNdYF-ACz-MnKMY0co6scWINU5V5wC1lyDn844k,
9
9
  django_bulk_hooks/manager.py,sha256=r54ct3S6AcqME2OsX-jPF944CEKcoSIW3qiAx_NwUaw,2801
10
10
  django_bulk_hooks/models.py,sha256=7RG7GrOdHXFjGVPV4FPRZVNMIHHW-hMCi6hn9LH_hVI,3331
11
11
  django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
12
- django_bulk_hooks/queryset.py,sha256=vAUzZDrBDQaxoR31DIYYTio7VlVxK_5RBAuTTQQ_M5g,16152
12
+ django_bulk_hooks/queryset.py,sha256=JKgm03XKFKYbg_vDdTkhB9JS9jQNZ3YVZILH7H94Qw0,17415
13
13
  django_bulk_hooks/registry.py,sha256=-mQBizJ06nz_tajZBinViKx_uP2Tbc1tIpTEMv7lwKA,705
14
- django_bulk_hooks-0.1.125.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
- django_bulk_hooks-0.1.125.dist-info/METADATA,sha256=uBRNf3cf9i9HAXYcA1uDyv2JOGYTd54pn9xt1VpaB50,6951
16
- django_bulk_hooks-0.1.125.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
- django_bulk_hooks-0.1.125.dist-info/RECORD,,
14
+ django_bulk_hooks-0.1.127.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
+ django_bulk_hooks-0.1.127.dist-info/METADATA,sha256=aGtzXvL39EPbGoSObDUs8Vp6sS8ONFT0zgQ0XFMfMCc,6951
16
+ django_bulk_hooks-0.1.127.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
+ django_bulk_hooks-0.1.127.dist-info/RECORD,,