django-bulk-hooks 0.2.67__py3-none-any.whl → 0.2.69__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.
- django_bulk_hooks/dispatcher.py +4 -0
- django_bulk_hooks/operations/coordinator.py +87 -4
- {django_bulk_hooks-0.2.67.dist-info → django_bulk_hooks-0.2.69.dist-info}/METADATA +1 -1
- {django_bulk_hooks-0.2.67.dist-info → django_bulk_hooks-0.2.69.dist-info}/RECORD +6 -6
- {django_bulk_hooks-0.2.67.dist-info → django_bulk_hooks-0.2.69.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.2.67.dist-info → django_bulk_hooks-0.2.69.dist-info}/WHEEL +0 -0
django_bulk_hooks/dispatcher.py
CHANGED
|
@@ -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
|
|
370
|
-
|
|
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
|
-
|
|
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,69 @@ 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
|
+
# AGGRESSIVE APPROACH: Also preload ALL relationship fields on the model
|
|
942
|
+
# This prevents N+1 queries from any relationship access during hook execution
|
|
943
|
+
try:
|
|
944
|
+
for field in self.model_cls._meta.get_fields():
|
|
945
|
+
if field.is_relation and not field.many_to_many and not field.one_to_many:
|
|
946
|
+
# This is a forward foreign key relationship
|
|
947
|
+
field_name = field.name
|
|
948
|
+
logger.info(f" 🔗 AUTO: Adding all relationship fields including {field_name}")
|
|
949
|
+
relationships.add(field_name)
|
|
950
|
+
except Exception as e:
|
|
951
|
+
logger.warning(f" ❌ Failed to extract all relationship fields: {e}")
|
|
952
|
+
|
|
953
|
+
logger.info(f"🔗 BULK PRELOAD: Total extracted relationships for {self.model_cls.__name__}: {list(relationships)}")
|
|
954
|
+
return relationships
|
|
955
|
+
|
|
873
956
|
def _cleanup_upsert_metadata(self, result_objects):
|
|
874
957
|
"""
|
|
875
958
|
Clean up temporary metadata added during upsert operations.
|
|
@@ -4,7 +4,7 @@ django_bulk_hooks/conditions.py,sha256=ar4pGjtxLKmgSIlO4S6aZFKmaBNchLtxMmWpkn4g9
|
|
|
4
4
|
django_bulk_hooks/constants.py,sha256=PxpEETaO6gdENcTPoXS586lerGKVP3nmjpDvOkmhYxI,509
|
|
5
5
|
django_bulk_hooks/context.py,sha256=mqaC5-yESDTA5ruI7fuXlt8qSgKuOFp0mjq7h1-4HdQ,1926
|
|
6
6
|
django_bulk_hooks/decorators.py,sha256=TdkO4FJyFrVU2zqK6Y_6JjEJ4v3nbKkk7aa22jN10sk,11994
|
|
7
|
-
django_bulk_hooks/dispatcher.py,sha256=
|
|
7
|
+
django_bulk_hooks/dispatcher.py,sha256=mNRiv9SEf9PwRskGA1GxTIl0kp-CozAcScICqpPHxzM,18809
|
|
8
8
|
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
9
9
|
django_bulk_hooks/factory.py,sha256=ezrVM5U023KZqOBbJXb6lYUP-pE7WJmi8Olh2Ew-7RA,18085
|
|
10
10
|
django_bulk_hooks/handler.py,sha256=SRCrMzgolrruTkvMnYBFmXLR-ABiw0JiH3605PEdCZM,4207
|
|
@@ -14,14 +14,14 @@ django_bulk_hooks/models.py,sha256=TWN_F-SsLGPx9jrkNT9pmJFR5VsZ0Z_QaVOZOmt7bpw,2
|
|
|
14
14
|
django_bulk_hooks/operations/__init__.py,sha256=BtJYjmRhe_sScivLsniDaZmBkm0ZLvcmzXFKL7QY2Xg,550
|
|
15
15
|
django_bulk_hooks/operations/analyzer.py,sha256=Pz8mc-EL8KDOfLQFYiRuN-r0OmINW3nIBhRJJCma-yo,10360
|
|
16
16
|
django_bulk_hooks/operations/bulk_executor.py,sha256=po8V_2H3ULiE0RYJ-wbaRIz52SKhss81UHwuQjlz3H8,26214
|
|
17
|
-
django_bulk_hooks/operations/coordinator.py,sha256=
|
|
17
|
+
django_bulk_hooks/operations/coordinator.py,sha256=NHQWg36IvWAsbQnAppsExOhFQsvlk_VD-aMwWsoo1ao,38572
|
|
18
18
|
django_bulk_hooks/operations/field_utils.py,sha256=cQ9w4xdk-z3PrMLFvRzVV07Wc0D2qbpSepwoupqwQH8,7888
|
|
19
19
|
django_bulk_hooks/operations/mti_handler.py,sha256=Vmz0C0gtYDvbybmb4cDzIaGglSaQK4DQVkaBK-WuQeE,25855
|
|
20
20
|
django_bulk_hooks/operations/mti_plans.py,sha256=HIRJgogHPpm6MV7nZZ-sZhMLUnozpZPV2SzwQHLRzYc,3667
|
|
21
21
|
django_bulk_hooks/operations/record_classifier.py,sha256=It85hJC2K-UsEOLbTR-QBdY5UPV-acQIJ91TSGa7pYo,7053
|
|
22
22
|
django_bulk_hooks/queryset.py,sha256=g_9OtOTC8FXY0hBwYr2FCqQ3mYXbfJTFPLlFV3SHmWQ,5600
|
|
23
23
|
django_bulk_hooks/registry.py,sha256=4HxP1mVK2z4VzvlohbEw2359wM21UJZJYagJJ1komM0,7947
|
|
24
|
-
django_bulk_hooks-0.2.
|
|
25
|
-
django_bulk_hooks-0.2.
|
|
26
|
-
django_bulk_hooks-0.2.
|
|
27
|
-
django_bulk_hooks-0.2.
|
|
24
|
+
django_bulk_hooks-0.2.69.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
25
|
+
django_bulk_hooks-0.2.69.dist-info/METADATA,sha256=HSHw428Qc1u1t--TmPEQmJLx4UcsIF7RHLwOjo798vU,9265
|
|
26
|
+
django_bulk_hooks-0.2.69.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
27
|
+
django_bulk_hooks-0.2.69.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|