django-bulk-hooks 0.2.73__tar.gz → 0.2.75__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.73 → django_bulk_hooks-0.2.75}/PKG-INFO +1 -1
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/dispatcher.py +74 -45
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/pyproject.toml +1 -1
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/LICENSE +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/README.md +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/changeset.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/factory.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/helpers.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/__init__.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/analyzer.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/bulk_executor.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/coordinator.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/field_utils.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/mti_handler.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/mti_plans.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/record_classifier.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/registry.py +0 -0
|
@@ -191,7 +191,55 @@ class HookDispatcher:
|
|
|
191
191
|
changeset: ChangeSet with all record changes
|
|
192
192
|
event: The hook event (e.g., 'before_create')
|
|
193
193
|
"""
|
|
194
|
-
#
|
|
194
|
+
# Use DI factory to create handler instance EARLY to access method decorators
|
|
195
|
+
from django_bulk_hooks.factory import create_hook_instance
|
|
196
|
+
|
|
197
|
+
handler = create_hook_instance(handler_cls)
|
|
198
|
+
method = getattr(handler, method_name)
|
|
199
|
+
|
|
200
|
+
# PRELOAD @select_related RELATIONSHIPS BEFORE CONDITION EVALUATION
|
|
201
|
+
# This ensures both conditions and hook methods have access to preloaded relationships
|
|
202
|
+
|
|
203
|
+
# Check if method has @select_related decorator
|
|
204
|
+
preload_func = getattr(method, "_select_related_preload", None)
|
|
205
|
+
if preload_func:
|
|
206
|
+
# Preload relationships to prevent N+1 queries in both conditions and hook methods
|
|
207
|
+
try:
|
|
208
|
+
model_cls_override = getattr(handler, "model_cls", None)
|
|
209
|
+
|
|
210
|
+
# Get FK fields being updated to avoid preloading conflicting relationships
|
|
211
|
+
skip_fields = changeset.operation_meta.get("fk_fields_being_updated", set())
|
|
212
|
+
|
|
213
|
+
# Preload for new_records (needed for condition evaluation and hook execution)
|
|
214
|
+
if changeset.new_records:
|
|
215
|
+
preload_func(
|
|
216
|
+
changeset.new_records,
|
|
217
|
+
model_cls=model_cls_override,
|
|
218
|
+
skip_fields=skip_fields,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Also preload for old_records (for conditions that check previous values)
|
|
222
|
+
if changeset.old_records:
|
|
223
|
+
preload_func(
|
|
224
|
+
changeset.old_records,
|
|
225
|
+
model_cls=model_cls_override,
|
|
226
|
+
skip_fields=skip_fields,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Mark that relationships have been preloaded to avoid duplicate condition preloading
|
|
230
|
+
changeset.operation_meta['relationships_preloaded'] = True
|
|
231
|
+
logger.debug(f"🔗 @select_related: Preloaded relationships for {handler_cls.__name__}.{method_name}")
|
|
232
|
+
|
|
233
|
+
except Exception as e:
|
|
234
|
+
logger.warning(f"Failed to preload relationships for {handler_cls.__name__}.{method_name}: {e}")
|
|
235
|
+
|
|
236
|
+
# SPECIAL HANDLING: Explicit @select_related support for BEFORE_CREATE hooks
|
|
237
|
+
# (This can stay for additional BEFORE_CREATE-specific logic if needed)
|
|
238
|
+
select_related_fields = getattr(method, "_select_related_fields", None)
|
|
239
|
+
if select_related_fields and event == "before_create" and changeset.new_records:
|
|
240
|
+
self._preload_select_related_for_before_create(changeset, select_related_fields)
|
|
241
|
+
|
|
242
|
+
# NOW condition evaluation is safe - relationships are preloaded
|
|
195
243
|
if condition:
|
|
196
244
|
# Skip per-hook preloading if relationships were already preloaded upfront
|
|
197
245
|
if not changeset.operation_meta.get('relationships_preloaded', False):
|
|
@@ -226,46 +274,6 @@ class HookDispatcher:
|
|
|
226
274
|
# No condition, use full changeset
|
|
227
275
|
filtered_changeset = changeset
|
|
228
276
|
|
|
229
|
-
# Use DI factory to create handler instance
|
|
230
|
-
from django_bulk_hooks.factory import create_hook_instance
|
|
231
|
-
|
|
232
|
-
handler = create_hook_instance(handler_cls)
|
|
233
|
-
method = getattr(handler, method_name)
|
|
234
|
-
|
|
235
|
-
# SPECIAL HANDLING: Explicit @select_related support for BEFORE_CREATE hooks
|
|
236
|
-
# This provides guaranteed bulk preloading to eliminate N+1 queries
|
|
237
|
-
select_related_fields = getattr(method, "_select_related_fields", None)
|
|
238
|
-
if select_related_fields and event == "before_create" and filtered_changeset.new_records:
|
|
239
|
-
self._preload_select_related_for_before_create(filtered_changeset, select_related_fields)
|
|
240
|
-
|
|
241
|
-
# Check if method has @select_related decorator (fallback for other cases)
|
|
242
|
-
preload_func = getattr(method, "_select_related_preload", None)
|
|
243
|
-
if preload_func:
|
|
244
|
-
# Preload relationships to prevent N+1 queries
|
|
245
|
-
try:
|
|
246
|
-
model_cls_override = getattr(handler, "model_cls", None)
|
|
247
|
-
|
|
248
|
-
# Get FK fields being updated to avoid preloading conflicting relationships
|
|
249
|
-
skip_fields = changeset.operation_meta.get("fk_fields_being_updated", set())
|
|
250
|
-
|
|
251
|
-
# Preload for new_records
|
|
252
|
-
if filtered_changeset.new_records:
|
|
253
|
-
preload_func(
|
|
254
|
-
filtered_changeset.new_records,
|
|
255
|
-
model_cls=model_cls_override,
|
|
256
|
-
skip_fields=skip_fields,
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
# Also preload for old_records (for conditions that check previous values)
|
|
260
|
-
if filtered_changeset.old_records:
|
|
261
|
-
preload_func(
|
|
262
|
-
filtered_changeset.old_records,
|
|
263
|
-
model_cls=model_cls_override,
|
|
264
|
-
skip_fields=skip_fields,
|
|
265
|
-
)
|
|
266
|
-
except Exception:
|
|
267
|
-
pass # Preload failed, continue without it
|
|
268
|
-
|
|
269
277
|
# Execute hook with ChangeSet
|
|
270
278
|
#
|
|
271
279
|
# ARCHITECTURE NOTE: Hook Contract
|
|
@@ -409,17 +417,38 @@ class HookDispatcher:
|
|
|
409
417
|
if hasattr(preloaded_obj, rel):
|
|
410
418
|
setattr(obj, rel, getattr(preloaded_obj, rel))
|
|
411
419
|
|
|
412
|
-
# Handle unsaved new_records by preloading their FK targets
|
|
420
|
+
# Handle unsaved new_records by preloading their FK targets (bulk query to avoid N+1)
|
|
413
421
|
if changeset.new_records:
|
|
422
|
+
# Collect FK IDs for each relationship from unsaved records
|
|
423
|
+
field_ids_map = {rel: set() for rel in relationship_list}
|
|
424
|
+
|
|
414
425
|
for obj in changeset.new_records:
|
|
415
426
|
if obj.pk is None: # Unsaved object
|
|
416
427
|
for rel in relationship_list:
|
|
417
428
|
if hasattr(obj, f'{rel}_id'):
|
|
418
429
|
rel_id = getattr(obj, f'{rel}_id')
|
|
419
430
|
if rel_id:
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
431
|
+
field_ids_map[rel].add(rel_id)
|
|
432
|
+
|
|
433
|
+
# Bulk load relationships for unsaved records
|
|
434
|
+
field_objects_map = {}
|
|
435
|
+
for rel, ids in field_ids_map.items():
|
|
436
|
+
if not ids:
|
|
437
|
+
continue
|
|
438
|
+
try:
|
|
439
|
+
rel_model = getattr(changeset.model_cls._meta.get_field(rel).remote_field, 'model')
|
|
440
|
+
field_objects_map[rel] = rel_model.objects.in_bulk(ids)
|
|
441
|
+
except Exception:
|
|
442
|
+
field_objects_map[rel] = {}
|
|
443
|
+
|
|
444
|
+
# Attach relationships to unsaved records
|
|
445
|
+
for obj in changeset.new_records:
|
|
446
|
+
if obj.pk is None: # Unsaved object
|
|
447
|
+
for rel in relationship_list:
|
|
448
|
+
rel_id = getattr(obj, f'{rel}_id', None)
|
|
449
|
+
if rel_id and rel in field_objects_map:
|
|
450
|
+
rel_obj = field_objects_map[rel].get(rel_id)
|
|
451
|
+
if rel_obj:
|
|
423
452
|
setattr(obj, rel, rel_obj)
|
|
424
453
|
|
|
425
454
|
def _preload_select_related_for_before_create(self, changeset, select_related_fields):
|
|
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
|
|
File without changes
|
{django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/__init__.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/analyzer.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/bulk_executor.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/coordinator.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/field_utils.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/mti_handler.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.73 → django_bulk_hooks-0.2.75}/django_bulk_hooks/operations/mti_plans.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|