django-bulk-hooks 0.1.126__tar.gz → 0.1.128__tar.gz

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.

Files changed (17) hide show
  1. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/django_bulk_hooks/queryset.py +68 -47
  3. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/LICENSE +0 -0
  5. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/README.md +0 -0
  6. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/django_bulk_hooks/conditions.py +0 -0
  8. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/django_bulk_hooks/constants.py +0 -0
  9. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/django_bulk_hooks/context.py +0 -0
  10. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/django_bulk_hooks/decorators.py +0 -0
  11. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/django_bulk_hooks/engine.py +0 -0
  12. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/django_bulk_hooks/enums.py +0 -0
  13. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/django_bulk_hooks/handler.py +0 -0
  14. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/django_bulk_hooks/manager.py +0 -0
  15. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/django_bulk_hooks/models.py +0 -0
  16. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/django_bulk_hooks/priority.py +0 -0
  17. {django_bulk_hooks-0.1.126 → django_bulk_hooks-0.1.128}/django_bulk_hooks/registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.126
3
+ Version: 0.1.128
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
@@ -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,31 +332,37 @@ 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
+ # Get the fields to insert (all local fields except auto fields)
338
+ fields = [f for f in parent_obj._meta.local_fields if not isinstance(f, AutoField)]
339
+ # For Django 4.2+, _do_insert requires fields, returning_fields, and raw parameters
340
+ parent_obj._do_insert(parent_obj._meta, fields, fields, False, using=self.db)
317
341
  parent_instances[model_class] = parent_obj
318
342
  current_parent = parent_obj
319
343
  parent_objects_map[id(obj)] = parent_instances
320
-
321
- # Step 2: Create and save child objects
344
+
345
+ # Step 2: Create all child objects and do single bulk insert into childmost table
322
346
  child_model = inheritance_chain[-1]
323
- created = []
347
+ all_child_objects = []
324
348
  for obj in batch:
325
349
  child_obj = self._create_child_instance(
326
350
  obj, child_model, parent_objects_map.get(id(obj), {})
327
351
  )
328
- # Save child object individually since Django's bulk_create doesn't support MTI
329
- # Use Django's Model.save() directly to avoid hooks but get proper field handling
330
- models.Model.save(child_obj)
331
- created.append(child_obj)
332
-
352
+ all_child_objects.append(child_obj)
353
+
354
+ # Step 2.5: Single bulk insert into childmost table
355
+ if all_child_objects:
356
+ # Use Django's internal bulk_create to bypass MTI exception
357
+ child_model._base_manager.bulk_create(all_child_objects)
358
+
333
359
  # Step 3: Update original objects with generated PKs and state
334
360
  pk_field_name = child_model._meta.pk.name
335
- for orig_obj, child_obj in zip(batch, created):
361
+ for orig_obj, child_obj in zip(batch, all_child_objects):
336
362
  setattr(orig_obj, pk_field_name, getattr(child_obj, pk_field_name))
337
363
  orig_obj._state.adding = False
338
364
  orig_obj._state.db = self.db
339
-
365
+
340
366
  return batch
341
367
 
342
368
  def _create_parent_instance(self, source_obj, parent_model, current_parent):
@@ -356,20 +382,18 @@ class HookQuerySet(models.QuerySet):
356
382
  ):
357
383
  setattr(parent_obj, field.name, current_parent)
358
384
  break
359
-
385
+
360
386
  # Handle auto_now_add and auto_now fields like Django does
361
387
  for field in parent_model._meta.local_fields:
362
- if hasattr(field, 'auto_now_add') and field.auto_now_add:
363
- field.pre_save(parent_obj, add=True)
364
- elif hasattr(field, 'auto_now') and field.auto_now:
365
- field.pre_save(parent_obj, add=True)
366
-
367
- # Ensure auto_now_add fields are explicitly set to prevent null constraint violations
368
- for field in parent_model._meta.local_fields:
369
- if hasattr(field, 'auto_now_add') and field.auto_now_add:
388
+ if hasattr(field, "auto_now_add") and field.auto_now_add:
389
+ # Ensure auto_now_add fields are properly set
370
390
  if getattr(parent_obj, field.name) is None:
371
391
  field.pre_save(parent_obj, add=True)
372
-
392
+ # Explicitly set the value to ensure it's not None
393
+ setattr(parent_obj, field.name, field.value_from_object(parent_obj))
394
+ elif hasattr(field, "auto_now") and field.auto_now:
395
+ field.pre_save(parent_obj, add=True)
396
+
373
397
  return parent_obj
374
398
 
375
399
  def _create_child_instance(self, source_obj, child_model, parent_instances):
@@ -385,19 +409,16 @@ class HookQuerySet(models.QuerySet):
385
409
  parent_link = child_model._meta.get_ancestor_link(parent_model)
386
410
  if parent_link:
387
411
  setattr(child_obj, parent_link.name, parent_instance)
388
-
412
+
389
413
  # Handle auto_now_add and auto_now fields like Django does
390
414
  for field in child_model._meta.local_fields:
391
- if hasattr(field, 'auto_now_add') and field.auto_now_add:
392
- field.pre_save(child_obj, add=True)
393
- elif hasattr(field, 'auto_now') and field.auto_now:
394
- field.pre_save(child_obj, add=True)
395
-
396
- # Ensure auto_now_add fields are explicitly set to prevent null constraint violations
397
- for field in child_model._meta.local_fields:
398
- if hasattr(field, 'auto_now_add') and field.auto_now_add:
415
+ if hasattr(field, "auto_now_add") and field.auto_now_add:
416
+ # Ensure auto_now_add fields are properly set
399
417
  if getattr(child_obj, field.name) is None:
400
418
  field.pre_save(child_obj, add=True)
401
-
402
- return child_obj
419
+ # Explicitly set the value to ensure it's not None
420
+ setattr(child_obj, field.name, field.value_from_object(child_obj))
421
+ elif hasattr(field, "auto_now") and field.auto_now:
422
+ field.pre_save(child_obj, add=True)
403
423
 
424
+ return child_obj
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.1.126"
3
+ version = "0.1.128"
4
4
  description = "Hook-style hooks for Django bulk operations like bulk_create and bulk_update."
5
5
  authors = ["Konrad Beck <konrad.beck@merchantcapital.co.za>"]
6
6
  readme = "README.md"