django-bulk-hooks 0.2.57__tar.gz → 0.2.59__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.57 → django_bulk_hooks-0.2.59}/PKG-INFO +1 -1
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/coordinator.py +42 -17
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/mti_handler.py +39 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/pyproject.toml +1 -1
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/LICENSE +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/README.md +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/changeset.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/dispatcher.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/factory.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/helpers.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/__init__.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/analyzer.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/bulk_executor.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/field_utils.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/mti_plans.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/record_classifier.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/registry.py +0 -0
{django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/coordinator.py
RENAMED
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
756
|
-
|
|
757
|
-
if
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
766
|
-
|
|
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
|
-
#
|
|
771
|
-
created_objects.
|
|
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.
|
|
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
|
|
{django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/mti_handler.py
RENAMED
|
@@ -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:
|
|
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
|
|
File without changes
|
{django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/__init__.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/analyzer.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/bulk_executor.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/field_utils.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.57 → django_bulk_hooks-0.2.59}/django_bulk_hooks/operations/mti_plans.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|