django-bulk-hooks 0.2.64__tar.gz → 0.2.66__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.
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/PKG-INFO +1 -1
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/dispatcher.py +73 -3
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/helpers.py +8 -1
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/pyproject.toml +1 -1
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/LICENSE +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/README.md +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/changeset.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/factory.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/__init__.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/analyzer.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/bulk_executor.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/coordinator.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/field_utils.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/mti_handler.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/mti_plans.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/record_classifier.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/registry.py +0 -0
|
@@ -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
|
-
#
|
|
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=
|
|
66
|
+
changed_fields=changed_fields,
|
|
60
67
|
)
|
|
61
68
|
for new in instances
|
|
62
69
|
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/__init__.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/analyzer.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/bulk_executor.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/coordinator.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/field_utils.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/mti_handler.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.64 → django_bulk_hooks-0.2.66}/django_bulk_hooks/operations/mti_plans.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|