django-bulk-hooks 0.2.66__tar.gz → 0.2.67__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 (27) hide show
  1. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/dispatcher.py +43 -19
  3. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/LICENSE +0 -0
  5. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/README.md +0 -0
  6. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/changeset.py +0 -0
  8. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/conditions.py +0 -0
  9. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/constants.py +0 -0
  10. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/context.py +0 -0
  11. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/decorators.py +0 -0
  12. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/enums.py +0 -0
  13. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/factory.py +0 -0
  14. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/handler.py +0 -0
  15. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/helpers.py +0 -0
  16. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/manager.py +0 -0
  17. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/models.py +0 -0
  18. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/operations/__init__.py +0 -0
  19. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/operations/analyzer.py +0 -0
  20. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/operations/bulk_executor.py +0 -0
  21. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/operations/coordinator.py +0 -0
  22. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/operations/field_utils.py +0 -0
  23. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/operations/mti_handler.py +0 -0
  24. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/operations/mti_plans.py +0 -0
  25. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/operations/record_classifier.py +0 -0
  26. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/django_bulk_hooks/queryset.py +0 -0
  27. {django_bulk_hooks-0.2.66 → django_bulk_hooks-0.2.67}/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.2.66
3
+ Version: 0.2.67
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
@@ -287,23 +287,36 @@ class HookDispatcher:
287
287
  """
288
288
  Preload relationships needed for condition evaluation.
289
289
 
290
+ This prevents N+1 queries when conditions access relationships on both
291
+ old_records and new_records (e.g., HasChanged conditions).
292
+
290
293
  Args:
291
294
  changeset: ChangeSet with records
292
295
  relationships: Set of relationship field names to preload
293
296
  """
294
- if not relationships or not changeset.new_records:
297
+ if not relationships:
295
298
  return
296
299
 
297
300
  # Use Django's select_related to preload relationships
298
301
  relationship_list = list(relationships)
299
302
 
300
- # Preload for new_records
303
+ # Collect all unique PKs from both new_records and old_records
304
+ all_ids = set()
305
+
306
+ # Add PKs from new_records
301
307
  if changeset.new_records:
302
- # Use select_related on the queryset
303
- ids = [obj.pk for obj in changeset.new_records if obj.pk is not None]
304
- if ids:
305
- preloaded = changeset.model_cls.objects.filter(pk__in=ids).select_related(*relationship_list).in_bulk()
306
- # Update the objects in changeset with preloaded relationships
308
+ all_ids.update(obj.pk for obj in changeset.new_records if obj.pk is not None)
309
+
310
+ # Add PKs from old_records
311
+ if changeset.old_records:
312
+ all_ids.update(obj.pk for obj in changeset.old_records if obj.pk is not None)
313
+
314
+ # Bulk preload relationships for all records that have PKs
315
+ if all_ids:
316
+ preloaded = changeset.model_cls.objects.filter(pk__in=list(all_ids)).select_related(*relationship_list).in_bulk()
317
+
318
+ # Update new_records with preloaded relationships
319
+ if changeset.new_records:
307
320
  for obj in changeset.new_records:
308
321
  if obj.pk and obj.pk in preloaded:
309
322
  preloaded_obj = preloaded[obj.pk]
@@ -311,17 +324,27 @@ class HookDispatcher:
311
324
  if hasattr(preloaded_obj, rel):
312
325
  setattr(obj, rel, getattr(preloaded_obj, rel))
313
326
 
314
- # Also handle unsaved objects by preloading their FK targets
315
- for obj in changeset.new_records:
316
- if obj.pk is None: # Unsaved object
317
- for rel in relationship_list:
318
- if hasattr(obj, f'{rel}_id'):
319
- rel_id = getattr(obj, f'{rel}_id')
320
- if rel_id:
321
- # Load the related object
322
- rel_model = getattr(changeset.model_cls._meta.get_field(rel).remote_field, 'model')
323
- rel_obj = rel_model.objects.get(pk=rel_id)
324
- setattr(obj, rel, rel_obj)
327
+ # Update old_records with preloaded relationships
328
+ if changeset.old_records:
329
+ for obj in changeset.old_records:
330
+ if obj.pk and obj.pk in preloaded:
331
+ preloaded_obj = preloaded[obj.pk]
332
+ for rel in relationship_list:
333
+ if hasattr(preloaded_obj, rel):
334
+ setattr(obj, rel, getattr(preloaded_obj, rel))
335
+
336
+ # Handle unsaved new_records by preloading their FK targets
337
+ if changeset.new_records:
338
+ for obj in changeset.new_records:
339
+ if obj.pk is None: # Unsaved object
340
+ for rel in relationship_list:
341
+ if hasattr(obj, f'{rel}_id'):
342
+ rel_id = getattr(obj, f'{rel}_id')
343
+ if rel_id:
344
+ # Load the related object
345
+ rel_model = getattr(changeset.model_cls._meta.get_field(rel).remote_field, 'model')
346
+ rel_obj = rel_model.objects.get(pk=rel_id)
347
+ setattr(obj, rel, rel_obj)
325
348
 
326
349
  def _preload_select_related_for_before_create(self, changeset, select_related_fields):
327
350
  """
@@ -336,7 +359,8 @@ class HookDispatcher:
336
359
  changeset: ChangeSet with new_records (unsaved objects)
337
360
  select_related_fields: List of field names to preload (e.g., ['financial_account'])
338
361
  """
339
- if not select_related_fields or not changeset.new_records:
362
+ # Ensure select_related_fields is actually iterable (not a Mock in tests)
363
+ if not select_related_fields or not changeset.new_records or not hasattr(select_related_fields, '__iter__'):
340
364
  return
341
365
 
342
366
  logger.info(f"🔗 BULK PRELOAD: Preloading {select_related_fields} for {len(changeset.new_records)} unsaved records")
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.2.66"
3
+ version = "0.2.67"
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"