django-bulk-hooks 0.2.64__py3-none-any.whl → 0.2.66__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.

@@ -110,9 +110,9 @@ class HookDispatcher:
110
110
  logger.info(f"🔥 HOOKS: Executing {len(hooks)} hooks for {changeset.model_cls.__name__}.{event}")
111
111
  for handler_cls, method_name, condition, priority in hooks:
112
112
  logger.info(f" → {handler_cls.__name__}.{method_name} (priority={priority})")
113
- self._execute_hook(handler_cls, method_name, condition, changeset)
113
+ self._execute_hook(handler_cls, method_name, condition, changeset, event)
114
114
 
115
- def _execute_hook(self, handler_cls, method_name, condition, changeset):
115
+ def _execute_hook(self, handler_cls, method_name, condition, changeset, event):
116
116
  """
117
117
  Execute a single hook with condition checking.
118
118
 
@@ -121,6 +121,7 @@ class HookDispatcher:
121
121
  method_name: Name of the method to call
122
122
  condition: Optional condition to filter records
123
123
  changeset: ChangeSet with all record changes
124
+ event: The hook event (e.g., 'before_create')
124
125
  """
125
126
  # NEW: Preload relationships needed for condition evaluation
126
127
  if condition:
@@ -155,7 +156,13 @@ class HookDispatcher:
155
156
  handler = create_hook_instance(handler_cls)
156
157
  method = getattr(handler, method_name)
157
158
 
158
- # Check if method has @select_related decorator
159
+ # SPECIAL HANDLING: Explicit @select_related support for BEFORE_CREATE hooks
160
+ # This provides guaranteed bulk preloading to eliminate N+1 queries
161
+ select_related_fields = getattr(method, "_select_related_fields", None)
162
+ if select_related_fields and event == "before_create" and filtered_changeset.new_records:
163
+ self._preload_select_related_for_before_create(filtered_changeset, select_related_fields)
164
+
165
+ # Check if method has @select_related decorator (fallback for other cases)
159
166
  preload_func = getattr(method, "_select_related_preload", None)
160
167
  if preload_func:
161
168
  # Preload relationships to prevent N+1 queries
@@ -316,6 +323,69 @@ class HookDispatcher:
316
323
  rel_obj = rel_model.objects.get(pk=rel_id)
317
324
  setattr(obj, rel, rel_obj)
318
325
 
326
+ def _preload_select_related_for_before_create(self, changeset, select_related_fields):
327
+ """
328
+ Explicit bulk preloading for @select_related on BEFORE_CREATE hooks.
329
+
330
+ This method provides guaranteed N+1 elimination by:
331
+ 1. Collecting all FK IDs from unsaved new_records
332
+ 2. Bulk querying related objects
333
+ 3. Attaching relationships to each record
334
+
335
+ Args:
336
+ changeset: ChangeSet with new_records (unsaved objects)
337
+ select_related_fields: List of field names to preload (e.g., ['financial_account'])
338
+ """
339
+ if not select_related_fields or not changeset.new_records:
340
+ return
341
+
342
+ logger.info(f"🔗 BULK PRELOAD: Preloading {select_related_fields} for {len(changeset.new_records)} unsaved records")
343
+
344
+ # Collect FK IDs for each field
345
+ field_ids_map = {field: set() for field in select_related_fields}
346
+
347
+ for record in changeset.new_records:
348
+ for field in select_related_fields:
349
+ fk_id = getattr(record, f'{field}_id', None)
350
+ if fk_id is not None:
351
+ field_ids_map[field].add(fk_id)
352
+
353
+ # Bulk query related objects for each field
354
+ field_objects_map = {}
355
+ for field, ids in field_ids_map.items():
356
+ if not ids:
357
+ continue
358
+
359
+ try:
360
+ # Get the related model
361
+ relation_field = changeset.model_cls._meta.get_field(field)
362
+ if not relation_field.is_relation:
363
+ continue
364
+
365
+ related_model = relation_field.remote_field.model
366
+
367
+ # Bulk query: related_model.objects.filter(id__in=ids)
368
+ field_objects_map[field] = related_model.objects.in_bulk(ids)
369
+ logger.info(f" ✅ Bulk loaded {len(field_objects_map[field])} {related_model.__name__} objects for field '{field}'")
370
+
371
+ except Exception as e:
372
+ logger.warning(f" ❌ Failed to bulk load field '{field}': {e}")
373
+ field_objects_map[field] = {}
374
+
375
+ # Attach relationships to each record
376
+ for record in changeset.new_records:
377
+ for field in select_related_fields:
378
+ fk_id = getattr(record, f'{field}_id', None)
379
+ if fk_id is not None and field in field_objects_map:
380
+ related_obj = field_objects_map[field].get(fk_id)
381
+ if related_obj is not None:
382
+ setattr(record, field, related_obj)
383
+ # Also cache in Django's fields_cache for consistency
384
+ if hasattr(record, '_state') and hasattr(record._state, 'fields_cache'):
385
+ record._state.fields_cache[field] = related_obj
386
+
387
+ logger.info(f"🔗 BULK PRELOAD: Completed relationship attachment for {len(changeset.new_records)} records")
388
+
319
389
 
320
390
  # Global dispatcher instance
321
391
  _dispatcher: HookDispatcher | None = None
@@ -52,11 +52,18 @@ def build_changeset_for_update(
52
52
  if old_records_map is None:
53
53
  old_records_map = {}
54
54
 
55
+ # Smart pre-computation logic:
56
+ # - If update_kwargs non-empty and old_records exist: Don't precompute (QuerySet.update case)
57
+ # - If update_kwargs empty and old_records exist: Don't precompute (upsert case)
58
+ # - If update_kwargs empty and no old_records: Precompute as empty (validation case)
59
+ should_precompute = not bool(update_kwargs) and old_records_map is None
60
+ changed_fields = list(update_kwargs.keys()) if should_precompute else None
61
+
55
62
  changes = [
56
63
  RecordChange(
57
64
  new,
58
65
  old_records_map.get(new.pk),
59
- changed_fields=list(update_kwargs.keys()),
66
+ changed_fields=changed_fields,
60
67
  )
61
68
  for new in instances
62
69
  ]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.2.64
3
+ Version: 0.2.66
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
@@ -4,11 +4,11 @@ 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=0mmfzLYP1YY8hMzf1wtI04-A3gz5H35ARt7VDno7OuU,13660
7
+ django_bulk_hooks/dispatcher.py,sha256=AQXryGHVdwjFJnyek2PcjZsQM9I6fVA74WLaPJeFPSw,17157
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
11
- django_bulk_hooks/helpers.py,sha256=tMxUI5oWhbtWByzCCR0Qcj1CgZ6iP5Jfx03EqVmEhxU,7597
11
+ django_bulk_hooks/helpers.py,sha256=3rH9TJkdCPF7Vu--0tDaZzJg9Yxcv7yoSF1K1_-0psQ,8048
12
12
  django_bulk_hooks/manager.py,sha256=aDuP87DZLWWbDK2qeA7usl3pxoIjHFIWnQNi_jEq6z0,4446
13
13
  django_bulk_hooks/models.py,sha256=TWN_F-SsLGPx9jrkNT9pmJFR5VsZ0Z_QaVOZOmt7bpw,2434
14
14
  django_bulk_hooks/operations/__init__.py,sha256=BtJYjmRhe_sScivLsniDaZmBkm0ZLvcmzXFKL7QY2Xg,550
@@ -21,7 +21,7 @@ django_bulk_hooks/operations/mti_plans.py,sha256=HIRJgogHPpm6MV7nZZ-sZhMLUnozpZP
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.64.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
25
- django_bulk_hooks-0.2.64.dist-info/METADATA,sha256=H1qQ5jw0HZoOXIx1KnTnxHoOBe7E8f08oqpGI-BU_IU,9265
26
- django_bulk_hooks-0.2.64.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
- django_bulk_hooks-0.2.64.dist-info/RECORD,,
24
+ django_bulk_hooks-0.2.66.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
25
+ django_bulk_hooks-0.2.66.dist-info/METADATA,sha256=SUi-L6kewPDyuGtGMW1PMJoV_DUxHlYAMv_x0kHNT9o,9265
26
+ django_bulk_hooks-0.2.66.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
+ django_bulk_hooks-0.2.66.dist-info/RECORD,,