django-bulk-hooks 0.2.12__tar.gz → 0.2.13__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 (25) hide show
  1. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/coordinator.py +83 -24
  3. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/pyproject.toml +1 -1
  4. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/LICENSE +0 -0
  5. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/README.md +0 -0
  6. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/__init__.py +0 -0
  7. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/changeset.py +0 -0
  8. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/conditions.py +0 -0
  9. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/constants.py +0 -0
  10. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/context.py +0 -0
  11. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/decorators.py +0 -0
  12. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/dispatcher.py +0 -0
  13. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/enums.py +0 -0
  14. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/factory.py +0 -0
  15. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/handler.py +0 -0
  16. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/helpers.py +0 -0
  17. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/manager.py +0 -0
  18. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/models.py +0 -0
  19. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/__init__.py +0 -0
  20. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/analyzer.py +0 -0
  21. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/bulk_executor.py +0 -0
  22. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/mti_handler.py +0 -0
  23. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/operations/mti_plans.py +0 -0
  24. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/django_bulk_hooks/queryset.py +0 -0
  25. {django_bulk_hooks-0.2.12 → django_bulk_hooks-0.2.13}/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.12
3
+ Version: 0.2.13
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
@@ -214,29 +214,28 @@ class BulkOperationCoordinator:
214
214
  """
215
215
  Execute queryset update with hooks - optimized for performance.
216
216
 
217
- ARCHITECTURE: Database-Level Update with AFTER-Only Hooks
218
- ==========================================================
217
+ ARCHITECTURE: Database-Level Update with Hook Support
218
+ =======================================================
219
219
 
220
- Uses native Django queryset.update() for maximum performance,
221
- then fetches results and runs AFTER hooks.
220
+ For queryset.update() operations:
221
+ 1. Fetch old state (before DB update)
222
+ 2. Execute native Django UPDATE (fast, direct SQL with Subquery/F() support)
223
+ 3. Fetch new state (after DB update, with computed values)
224
+ 4. Run BEFORE_UPDATE hooks with old/new state
225
+ - Hooks can see Subquery-computed values via new_records
226
+ - Hooks CAN modify instances (e.g., set derived fields)
227
+ - Modifications are auto-persisted with bulk_update
228
+ 5. Run AFTER_UPDATE hooks (for read-only side effects)
222
229
 
223
- This approach:
224
- - ✅ Uses native SQL UPDATE (fastest for aggregations, F(), Subquery)
225
- - ✅ Maintains framework patterns (MTI support, changeset building)
226
- - ✅ Runs AFTER_UPDATE hooks with old/new state
227
- - ❌ BEFORE_UPDATE hooks cannot modify values (use bulk_update() instead)
230
+ Total DML: 1 (queryset.update) + 1 (bulk_update if hooks modify anything)
228
231
 
229
- For cases where BEFORE hooks need to modify values, use bulk_update():
230
- - MyModel.objects.bulk_update(objs, fields) # Allows BEFORE modifications
231
- - MyModel.objects.update(...) # Fast, AFTER hooks only
232
-
233
- Args:
234
- update_kwargs: Dict of fields to update
235
- bypass_hooks: Skip all hooks if True
236
- bypass_validation: Skip validation hooks if True
237
-
238
- Returns:
239
- Number of objects updated
232
+ Note: BEFORE_UPDATE runs AFTER the primary database update.
233
+ This enables:
234
+ - HasChanged conditions to work with Subquery-computed values
235
+ - Cascading updates from hook modifications
236
+ - Optimal performance (Subquery stays in SQL)
237
+
238
+ For true BEFORE semantics (prevent/modify before DB write), use bulk_update().
240
239
  """
241
240
  # Check bypass early
242
241
  from django_bulk_hooks.context import get_bypass_hooks
@@ -258,8 +257,8 @@ class BulkOperationCoordinator:
258
257
  """
259
258
  Execute queryset update with hooks - fast path using native Django update.
260
259
 
261
- This method follows framework patterns but is optimized for database-level
262
- operations where BEFORE hooks cannot modify values.
260
+ This method provides full hook lifecycle support for queryset.update()
261
+ including BEFORE_UPDATE hooks with automatic persistence of modifications.
263
262
 
264
263
  Args:
265
264
  update_kwargs: Dict of fields to update
@@ -293,14 +292,74 @@ class BulkOperationCoordinator:
293
292
 
294
293
  # Mark that this is a queryset update (for potential hook inspection)
295
294
  changeset.operation_meta['is_queryset_update'] = True
296
- changeset.operation_meta['allows_before_modifications'] = False
295
+ changeset.operation_meta['allows_before_modifications'] = True
297
296
 
298
297
  # 5. Get MTI chain (follow framework pattern)
299
298
  models_in_chain = [self.model_cls]
300
299
  if self.mti_handler.is_mti_model():
301
300
  models_in_chain.extend(self.mti_handler.get_parent_models())
302
301
 
303
- # 6. AFTER hooks only (following MTI pattern from _execute_with_mti_hooks)
302
+ # 6. BEFORE_UPDATE hooks (with auto-persistence)
303
+ # Snapshot state before hooks
304
+ pre_hook_state = {}
305
+ for instance in new_instances:
306
+ if instance.pk is not None:
307
+ pre_hook_values = {}
308
+ for field in self.model_cls._meta.fields:
309
+ try:
310
+ pre_hook_values[field.name] = getattr(instance, field.name, None)
311
+ except Exception:
312
+ pre_hook_values[field.name] = None
313
+ pre_hook_state[instance.pk] = pre_hook_values
314
+
315
+ # Dispatch BEFORE_UPDATE hooks
316
+ for model_cls in models_in_chain:
317
+ model_changeset = self._build_changeset_for_model(changeset, model_cls)
318
+ self.dispatcher.dispatch(model_changeset, "before_update", bypass_hooks=False)
319
+
320
+ # Detect modifications made by BEFORE_UPDATE hooks
321
+ hook_modified_fields = set()
322
+ for instance in new_instances:
323
+ if instance.pk in pre_hook_state:
324
+ for field_name, pre_value in pre_hook_state[instance.pk].items():
325
+ try:
326
+ current_value = getattr(instance, field_name, None)
327
+ except Exception:
328
+ current_value = None
329
+
330
+ if current_value != pre_value:
331
+ hook_modified_fields.add(field_name)
332
+
333
+ # Auto-persist hook modifications
334
+ if hook_modified_fields:
335
+ logger.info(
336
+ f"BEFORE_UPDATE hooks modified {len(hook_modified_fields)} fields: {hook_modified_fields}"
337
+ )
338
+ logger.info("Auto-persisting modifications with bulk_update")
339
+
340
+ # Use bulk_update to persist changes
341
+ # This will trigger another hook cycle (Salesforce-style cascading)
342
+ from django.db.models import QuerySet as BaseQuerySet
343
+ base_qs = BaseQuerySet(model=self.model_cls, using=self.queryset.db)
344
+ base_qs.bulk_update(new_instances, list(hook_modified_fields))
345
+
346
+ # Refresh instances after bulk_update to reflect any changes
347
+ refreshed_map = {
348
+ inst.pk: inst
349
+ for inst in self.model_cls.objects.filter(
350
+ pk__in=[obj.pk for obj in new_instances]
351
+ )
352
+ }
353
+ for instance in new_instances:
354
+ if instance.pk in refreshed_map:
355
+ refreshed = refreshed_map[instance.pk]
356
+ for field in self.model_cls._meta.fields:
357
+ try:
358
+ setattr(instance, field.name, getattr(refreshed, field.name))
359
+ except Exception:
360
+ pass
361
+
362
+ # 7. AFTER_UPDATE hooks (read-only side effects)
304
363
  for model_cls in models_in_chain:
305
364
  model_changeset = self._build_changeset_for_model(changeset, model_cls)
306
365
  self.dispatcher.dispatch(model_changeset, "after_update", bypass_hooks=False)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.2.12"
3
+ version = "0.2.13"
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"