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

@@ -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
 
@@ -306,6 +306,14 @@ class MTIHandler:
306
306
  if not filtered_updates and normalized_unique:
307
307
  filtered_updates = [normalized_unique[0]]
308
308
 
309
+ # CRITICAL FIX: Always include auto_now fields in updates to ensure timestamps are updated.
310
+ # During MTI upsert, parent tables need auto_now fields updated even when only child fields change.
311
+ # This ensures parent-level timestamps (e.g., updated_at) refresh correctly on upsert.
312
+ auto_now_fields = self._get_auto_now_fields_for_model(model_class, model_fields_by_name)
313
+ if auto_now_fields:
314
+ # Convert to set to avoid duplicates, then back to list for consistency
315
+ filtered_updates = list(set(filtered_updates) | set(auto_now_fields))
316
+
309
317
  # Only enable upsert if we have fields to update (real or dummy)
310
318
  if filtered_updates:
311
319
  level_update_conflicts = True
@@ -334,6 +342,14 @@ class MTIHandler:
334
342
  ]
335
343
  level_update_fields = available_fields[:1] if available_fields else [pk_field.name]
336
344
 
345
+ # CRITICAL FIX: Include auto_now fields in update_fields to ensure timestamps are updated.
346
+ # During MTI upsert, parent tables need auto_now fields updated even when using dummy fields.
347
+ # This ensures parent-level timestamps (e.g., updated_at) refresh correctly on upsert.
348
+ auto_now_fields = self._get_auto_now_fields_for_model(model_class, model_fields_by_name)
349
+ if auto_now_fields:
350
+ # Convert to set to avoid duplicates, then back to list for consistency
351
+ level_update_fields = list(set(level_update_fields) | set(auto_now_fields))
352
+
337
353
  # Create parent level
338
354
  parent_level = ParentLevel(
339
355
  model_class=model_class,
@@ -347,6 +363,29 @@ class MTIHandler:
347
363
 
348
364
  return parent_levels
349
365
 
366
+ def _get_auto_now_fields_for_model(self, model_class, model_fields_by_name):
367
+ """
368
+ Get auto_now (not auto_now_add) fields for a specific model.
369
+
370
+ Only includes fields that exist in model_fields_by_name to ensure
371
+ they're valid local fields for this model level.
372
+
373
+ Args:
374
+ model_class: Model class to get fields for
375
+ model_fields_by_name: Dict of valid field names for this model level
376
+
377
+ Returns:
378
+ List of auto_now field names (excluding auto_now_add)
379
+ """
380
+ auto_now_fields = []
381
+ for field in model_class._meta.local_fields:
382
+ # Only include auto_now (not auto_now_add) since auto_now_add should only be set on creation
383
+ if getattr(field, "auto_now", False) and not getattr(field, "auto_now_add", False):
384
+ # Double-check field exists in model_fields_by_name for safety
385
+ if field.name in model_fields_by_name:
386
+ auto_now_fields.append(field.name)
387
+ return auto_now_fields
388
+
350
389
  def _has_matching_constraint(self, model_class, normalized_unique):
351
390
  """Check if model has a unique constraint matching the given fields."""
352
391
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.2.57
3
+ Version: 0.2.59
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
@@ -14,14 +14,14 @@ django_bulk_hooks/models.py,sha256=4Vvi2LiGP0g4j08a5liqBROfsO8Wd_ermBoyjKwfrPU,2
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
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
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=MMDxuE6jzwkhMwVE-6qJUdp4MNKgAIFPMjwdN_umrqs,23503
19
+ django_bulk_hooks/operations/mti_handler.py,sha256=173jghcxCE5UEZxM1QJRS-lWg0-KJxCQCbWHVKppIEM,26000
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.57.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
25
- django_bulk_hooks-0.2.57.dist-info/METADATA,sha256=SWZOMPOgiBDCLuSxHX9xTeJMqcd3CgvDZP-JuoY4wRs,9265
26
- django_bulk_hooks-0.2.57.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
- django_bulk_hooks-0.2.57.dist-info/RECORD,,
24
+ django_bulk_hooks-0.2.59.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
25
+ django_bulk_hooks-0.2.59.dist-info/METADATA,sha256=Zqy1xIlJ4pSLl-rxnjA96tNhdgomhT21cnHI7Z9ZbE8,9265
26
+ django_bulk_hooks-0.2.59.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
27
+ django_bulk_hooks-0.2.59.dist-info/RECORD,,