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

@@ -182,10 +182,35 @@ class BulkExecutor:
182
182
  if not objs:
183
183
  return 0
184
184
 
185
+ # Ensure auto_now fields are included and pre-saved for all models
186
+ # This handles both MTI and non-MTI models uniformly (SOC & DRY)
187
+ fields = list(fields) # Make a copy so we can modify it
188
+
189
+ # Get models to check - for MTI, check entire inheritance chain
190
+ if self.mti_handler.is_mti_model():
191
+ models_to_check = self.mti_handler.get_inheritance_chain()
192
+ else:
193
+ models_to_check = [self.model_cls]
194
+
195
+ # Collect all auto_now fields and pre-save them
196
+ auto_now_fields = set()
197
+ for model in models_to_check:
198
+ for field in model._meta.local_fields:
199
+ if getattr(field, "auto_now", False) and not getattr(field, "auto_now_add", False):
200
+ auto_now_fields.add(field.name)
201
+ # Pre-save the field to set the value on instances
202
+ for obj in objs:
203
+ field.pre_save(obj, add=False)
204
+
205
+ # Add auto_now fields to the update list if not already present
206
+ for auto_now_field in auto_now_fields:
207
+ if auto_now_field not in fields:
208
+ fields.append(auto_now_field)
209
+
185
210
  # Check if this is an MTI model and route accordingly
186
211
  if self.mti_handler.is_mti_model():
187
212
  logger.info(f"Detected MTI model {self.model_cls.__name__}, using MTI bulk update")
188
- # Build execution plan
213
+ # Build execution plan (fields already have auto_now included)
189
214
  plan = self.mti_handler.build_update_plan(objs, fields, batch_size=batch_size)
190
215
  # Execute the plan
191
216
  return self._execute_mti_update_plan(plan)
@@ -740,6 +740,8 @@ class BulkOperationCoordinator:
740
740
  if not result_objects:
741
741
  return
742
742
 
743
+ # First pass: collect objects with metadata and objects needing timestamp check
744
+ objects_needing_timestamp_check = []
743
745
  for obj in result_objects:
744
746
  # Check if metadata was set
745
747
  if hasattr(obj, "_bulk_hooks_was_created"):
@@ -749,29 +751,52 @@ class BulkOperationCoordinator:
749
751
  else:
750
752
  updated_objects.append(obj)
751
753
  else:
752
- # Fallback: if no metadata, check timestamps
754
+ # Need to check timestamps - collect for bulk query
755
+ objects_needing_timestamp_check.append(obj)
756
+
757
+ # Bulk fetch timestamps for objects without metadata (avoids N+1 queries)
758
+ if objects_needing_timestamp_check:
759
+ # Group by model class to handle MTI scenarios
760
+ objects_by_model = {}
761
+ for obj in objects_needing_timestamp_check:
753
762
  model_cls = obj.__class__
763
+ if model_cls not in objects_by_model:
764
+ objects_by_model[model_cls] = []
765
+ objects_by_model[model_cls].append(obj)
766
+
767
+ # Fetch timestamps in bulk for each model class
768
+ for model_cls, objs in objects_by_model.items():
754
769
  if hasattr(model_cls, "created_at") and hasattr(model_cls, "updated_at"):
755
- # Reload from DB to get accurate timestamps
756
- db_obj = model_cls.objects.filter(pk=obj.pk).values("created_at", "updated_at").first()
757
- if db_obj:
758
- created_at = db_obj["created_at"]
759
- updated_at = db_obj["updated_at"]
760
- if created_at and updated_at:
761
- time_diff = abs((updated_at - created_at).total_seconds())
762
- if time_diff <= 1.0: # Within 1 second = just created
763
- created_objects.append(obj)
770
+ # Bulk fetch timestamps for all objects of this model
771
+ pks = [obj.pk for obj in objs if obj.pk is not None]
772
+ if pks:
773
+ timestamp_map = {
774
+ record["pk"]: (record["created_at"], record["updated_at"])
775
+ for record in model_cls.objects.filter(pk__in=pks).values("pk", "created_at", "updated_at")
776
+ }
777
+
778
+ # Classify each object based on timestamps
779
+ for obj in objs:
780
+ if obj.pk in timestamp_map:
781
+ created_at, updated_at = timestamp_map[obj.pk]
782
+ if created_at and updated_at:
783
+ time_diff = abs((updated_at - created_at).total_seconds())
784
+ if time_diff <= 1.0: # Within 1 second = just created
785
+ created_objects.append(obj)
786
+ else:
787
+ updated_objects.append(obj)
788
+ else:
789
+ # No timestamps, default to created
790
+ created_objects.append(obj)
764
791
  else:
765
- updated_objects.append(obj)
766
- else:
767
- # No timestamps, default to created
768
- created_objects.append(obj)
792
+ # Object not found, treat as created
793
+ created_objects.append(obj)
769
794
  else:
770
- # Object not found, treat as created
771
- created_objects.append(obj)
795
+ # No PKs, default all to created
796
+ created_objects.extend(objs)
772
797
  else:
773
798
  # No timestamp fields, default to created
774
- created_objects.append(obj)
799
+ created_objects.extend(objs)
775
800
 
776
801
  logger.info(f"Upsert after hooks: {len(created_objects)} created, {len(updated_objects)} updated")
777
802
 
@@ -537,7 +537,7 @@ class MTIHandler:
537
537
 
538
538
  Args:
539
539
  objs: List of model instances to update
540
- fields: List of field names to update
540
+ fields: List of field names to update (auto_now fields already included by executor)
541
541
  batch_size: Number of objects per batch
542
542
 
543
543
  Returns:
@@ -555,28 +555,15 @@ class MTIHandler:
555
555
 
556
556
  batch_size = batch_size or len(objs)
557
557
 
558
- # Handle auto_now fields
559
- for obj in objs:
560
- for model in inheritance_chain:
561
- for field in model._meta.local_fields:
562
- if getattr(field, "auto_now", False):
563
- field.pre_save(obj, add=False)
564
-
565
- # Add auto_now fields to update list
566
- auto_now_fields = set()
567
- for model in inheritance_chain:
568
- for field in model._meta.local_fields:
569
- if getattr(field, "auto_now", False):
570
- auto_now_fields.add(field.name)
571
-
572
- all_fields = list(fields) + list(auto_now_fields)
558
+ # Note: auto_now fields are already handled by executor.bulk_update()
559
+ # which calls pre_save() and includes them in the fields list
573
560
 
574
561
  # Group fields by model
575
562
  field_groups = []
576
563
  for model_idx, model in enumerate(inheritance_chain):
577
564
  model_fields = []
578
565
 
579
- for field_name in all_fields:
566
+ for field_name in fields:
580
567
  try:
581
568
  field = self.model_cls._meta.get_field(field_name)
582
569
  if field in model._meta.local_fields:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.2.58
3
+ Version: 0.2.60
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
@@ -13,15 +13,15 @@ django_bulk_hooks/manager.py,sha256=3mFzB0ZzHHeXWdKGObZD_H0NlskHJc8uYBF69KKdAXU,
13
13
  django_bulk_hooks/models.py,sha256=4Vvi2LiGP0g4j08a5liqBROfsO8Wd_ermBoyjKwfrPU,2512
14
14
  django_bulk_hooks/operations/__init__.py,sha256=BtJYjmRhe_sScivLsniDaZmBkm0ZLvcmzXFKL7QY2Xg,550
15
15
  django_bulk_hooks/operations/analyzer.py,sha256=wAG8sAG9NwfwNqG9z81VfGR7AANDzRmMGE_o82MWji4,10689
16
- django_bulk_hooks/operations/bulk_executor.py,sha256=Y-wkvuV_X-SZmI965JVrrtwbzPZVggUfy8mR1pzP9d0,27048
17
- django_bulk_hooks/operations/coordinator.py,sha256=iGavJLqe3eYRqFay8cMn6muwyRYzQo-HFGphsS5hL6g,30799
16
+ django_bulk_hooks/operations/bulk_executor.py,sha256=0_r9qaOMl9RJpi9eE3GrdUAlXA6ZU016nHYeIZ-yiPE,28279
17
+ django_bulk_hooks/operations/coordinator.py,sha256=1Ka5eZJXTFjx3tr-BD6Tr350Y2T57SUOX3vjagBYBvM,32193
18
18
  django_bulk_hooks/operations/field_utils.py,sha256=Tvr5bcZLG8imH-r2S85oui1Cbw6hGv3VtuIMn4OvsU4,2895
19
- django_bulk_hooks/operations/mti_handler.py,sha256=173jghcxCE5UEZxM1QJRS-lWg0-KJxCQCbWHVKppIEM,26000
19
+ django_bulk_hooks/operations/mti_handler.py,sha256=pguym-6vqaLxu-lY7gnb5laKT9RJQlUciNw1SflZ1Zs,25574
20
20
  django_bulk_hooks/operations/mti_plans.py,sha256=7STQ2oA2ZT8cEG3-t-6xciRAdf7OeSf0gRLXR_BRG-Q,3363
21
21
  django_bulk_hooks/operations/record_classifier.py,sha256=kqML4aO11X9K3SSJ5DUlUukwI172j_Tk12Kr77ee8q8,7065
22
22
  django_bulk_hooks/queryset.py,sha256=aQitlbexcVnmeAdc0jtO3hci39p4QEu4srQPEzozy5s,5546
23
23
  django_bulk_hooks/registry.py,sha256=uum5jhGI3TPaoiXuA1MdBdu4gbE3rQGGwQ5YDjiMcjk,7949
24
- django_bulk_hooks-0.2.58.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
25
- django_bulk_hooks-0.2.58.dist-info/METADATA,sha256=kVna-_dN9xVNaMMZHZUzm-Ab4IjhVzA1L8XXPms5T7o,9265
26
- django_bulk_hooks-0.2.58.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
- django_bulk_hooks-0.2.58.dist-info/RECORD,,
24
+ django_bulk_hooks-0.2.60.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
25
+ django_bulk_hooks-0.2.60.dist-info/METADATA,sha256=tYgGJtWSG7Zx360peLawbsJAiIkT-BwF7WuJg4JfkJM,9265
26
+ django_bulk_hooks-0.2.60.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
+ django_bulk_hooks-0.2.60.dist-info/RECORD,,