django-bulk-hooks 0.2.67__tar.gz → 0.2.68__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.67 → django_bulk_hooks-0.2.68}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/dispatcher.py +4 -0
  3. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/operations/coordinator.py +75 -4
  4. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/pyproject.toml +1 -1
  5. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/LICENSE +0 -0
  6. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/README.md +0 -0
  7. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/__init__.py +0 -0
  8. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/changeset.py +0 -0
  9. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/conditions.py +0 -0
  10. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/constants.py +0 -0
  11. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/context.py +0 -0
  12. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/decorators.py +0 -0
  13. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/enums.py +0 -0
  14. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/factory.py +0 -0
  15. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/handler.py +0 -0
  16. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/helpers.py +0 -0
  17. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/manager.py +0 -0
  18. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/models.py +0 -0
  19. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/operations/__init__.py +0 -0
  20. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/operations/analyzer.py +0 -0
  21. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/operations/bulk_executor.py +0 -0
  22. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/operations/field_utils.py +0 -0
  23. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/operations/mti_handler.py +0 -0
  24. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/operations/mti_plans.py +0 -0
  25. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/operations/record_classifier.py +0 -0
  26. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/django_bulk_hooks/queryset.py +0 -0
  27. {django_bulk_hooks-0.2.67 → django_bulk_hooks-0.2.68}/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.67
3
+ Version: 0.2.68
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
@@ -126,12 +126,16 @@ class HookDispatcher:
126
126
  # NEW: Preload relationships needed for condition evaluation
127
127
  if condition:
128
128
  condition_relationships = self._extract_condition_relationships(condition, changeset.model_cls)
129
+ logger.info(f"🔍 CONDITION: {handler_cls.__name__}.{method_name} has condition, extracted relationships: {condition_relationships}")
129
130
  if condition_relationships:
131
+ logger.info(f"🔗 PRELOADING: Preloading condition relationships for {len(changeset.changes)} records")
130
132
  self._preload_condition_relationships(changeset, condition_relationships)
131
133
 
132
134
  # Filter records based on condition (now safe - relationships are preloaded)
133
135
  if condition:
136
+ logger.info(f"⚡ EVALUATING: Checking condition for {handler_cls.__name__}.{method_name} on {len(changeset.changes)} records")
134
137
  filtered_changes = [change for change in changeset.changes if condition.check(change.new_record, change.old_record)]
138
+ logger.info(f"✅ CONDITION: {len(filtered_changes)}/{len(changeset.changes)} records passed condition filter")
135
139
 
136
140
  if not filtered_changes:
137
141
  # No records match condition, skip this hook
@@ -366,8 +366,19 @@ class BulkOperationCoordinator:
366
366
  Returns:
367
367
  Number of rows updated
368
368
  """
369
- # Step 1: Fetch old state (before database update)
370
- old_instances = list(self.queryset)
369
+ # Step 0: Extract relationships that hooks might access to avoid N+1 queries
370
+ hook_relationships = self._extract_hook_relationships()
371
+
372
+ # Step 1: Fetch old state (before database update) with relationships preloaded
373
+ if hook_relationships:
374
+ logger.info(f"🔗 BULK PRELOAD: Fetching {self.queryset.count()} old instances with select_related({list(hook_relationships)})")
375
+ old_queryset = self.queryset.select_related(*hook_relationships)
376
+ else:
377
+ logger.info(f"🔗 BULK PRELOAD: Fetching {self.queryset.count()} old instances without select_related")
378
+ old_queryset = self.queryset
379
+
380
+ old_instances = list(old_queryset)
381
+ logger.info(f"✅ Fetched {len(old_instances)} old instances")
371
382
  if not old_instances:
372
383
  return 0
373
384
 
@@ -380,11 +391,20 @@ class BulkOperationCoordinator:
380
391
  if update_count == 0:
381
392
  return 0
382
393
 
383
- # Step 3: Fetch new state (after database update)
394
+ # Step 3: Fetch new state (after database update) with relationships preloaded
384
395
  # This captures any Subquery/F() computed values
385
396
  # Use primary keys to fetch updated instances since queryset filters may no longer match
386
397
  pks = [inst.pk for inst in old_instances]
387
- new_instances = list(self.model_cls.objects.filter(pk__in=pks))
398
+
399
+ if hook_relationships:
400
+ logger.info(f"🔗 BULK PRELOAD: Fetching {len(pks)} new instances with select_related({list(hook_relationships)})")
401
+ new_queryset = self.model_cls.objects.filter(pk__in=pks).select_related(*hook_relationships)
402
+ else:
403
+ logger.info(f"🔗 BULK PRELOAD: Fetching {len(pks)} new instances without select_related")
404
+ new_queryset = self.model_cls.objects.filter(pk__in=pks)
405
+
406
+ new_instances = list(new_queryset)
407
+ logger.info(f"✅ Fetched {len(new_instances)} new instances")
388
408
 
389
409
  # Step 4: Build changeset
390
410
  changeset = build_changeset_for_update(
@@ -870,6 +890,57 @@ class BulkOperationCoordinator:
870
890
  # Clean up temporary metadata
871
891
  self._cleanup_upsert_metadata(result_objects)
872
892
 
893
+ def _extract_hook_relationships(self):
894
+ """
895
+ Extract all relationship paths that hooks might access for this model and its MTI parents.
896
+
897
+ This prevents N+1 queries by preloading all relationships that any hook
898
+ (condition or @select_related) might access during bulk operations.
899
+
900
+ Returns:
901
+ set: Set of relationship field names to preload with select_related
902
+ """
903
+ relationships = set()
904
+
905
+ # Get the dispatcher to access hook registry
906
+ dispatcher = self.dispatcher
907
+
908
+ # Get all models in the inheritance chain (including parents for MTI)
909
+ models_to_check = self._get_models_in_chain(self.model_cls)
910
+
911
+ # Check hooks for all relevant events that might run during bulk operations
912
+ events_to_check = ['before_update', 'after_update', 'validate_update']
913
+
914
+ for model_cls in models_to_check:
915
+ logger.info(f"🔍 BULK PRELOAD: Checking hooks for model {model_cls.__name__}")
916
+ for event in events_to_check:
917
+ hooks = dispatcher.registry.get_hooks(model_cls, event)
918
+ logger.info(f" 🔍 Found {len(hooks)} hooks for {model_cls.__name__}.{event}")
919
+
920
+ for handler_cls, method_name, condition, priority in hooks:
921
+ logger.info(f" → Checking {handler_cls.__name__}.{method_name}")
922
+
923
+ # Extract relationships from conditions
924
+ if condition:
925
+ condition_relationships = dispatcher._extract_condition_relationships(condition, model_cls)
926
+ if condition_relationships:
927
+ logger.info(f" 📋 Condition relationships for {model_cls.__name__}: {condition_relationships}")
928
+ relationships.update(condition_relationships)
929
+
930
+ # Extract relationships from @select_related decorators
931
+ try:
932
+ method = getattr(handler_cls, method_name, None)
933
+ if method:
934
+ select_related_fields = getattr(method, "_select_related_fields", None)
935
+ if select_related_fields and hasattr(select_related_fields, '__iter__'):
936
+ logger.info(f" 🔗 @select_related fields on {handler_cls.__name__}.{method_name}: {list(select_related_fields)}")
937
+ relationships.update(select_related_fields)
938
+ except Exception as e:
939
+ logger.warning(f" ❌ Failed to extract @select_related from {handler_cls.__name__}.{method_name}: {e}")
940
+
941
+ logger.info(f"🔗 BULK PRELOAD: Total extracted relationships for {self.model_cls.__name__}: {list(relationships)}")
942
+ return relationships
943
+
873
944
  def _cleanup_upsert_metadata(self, result_objects):
874
945
  """
875
946
  Clean up temporary metadata added during upsert operations.
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.2.67"
3
+ version = "0.2.68"
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"