django-bulk-hooks 0.1.264__py3-none-any.whl → 0.1.266__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.
- django_bulk_hooks/manager.py +4 -0
- django_bulk_hooks/queryset.py +245 -93
- {django_bulk_hooks-0.1.264.dist-info → django_bulk_hooks-0.1.266.dist-info}/METADATA +1 -1
- {django_bulk_hooks-0.1.264.dist-info → django_bulk_hooks-0.1.266.dist-info}/RECORD +6 -6
- {django_bulk_hooks-0.1.264.dist-info → django_bulk_hooks-0.1.266.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.264.dist-info → django_bulk_hooks-0.1.266.dist-info}/WHEEL +0 -0
django_bulk_hooks/manager.py
CHANGED
|
@@ -56,6 +56,10 @@ class BulkHookManager(models.Manager):
|
|
|
56
56
|
"""
|
|
57
57
|
Delegate to QuerySet's bulk_update implementation.
|
|
58
58
|
This follows Django's pattern where Manager methods call QuerySet methods.
|
|
59
|
+
|
|
60
|
+
Note: Parameters like unique_fields, update_conflicts, update_fields, and ignore_conflicts
|
|
61
|
+
are not supported by bulk_update and will be ignored with a warning.
|
|
62
|
+
These parameters are only available in bulk_create for UPSERT operations.
|
|
59
63
|
"""
|
|
60
64
|
return self.get_queryset().bulk_update(
|
|
61
65
|
objs,
|
django_bulk_hooks/queryset.py
CHANGED
|
@@ -51,7 +51,11 @@ class HookQuerySetMixin:
|
|
|
51
51
|
if obj.pk is not None:
|
|
52
52
|
# Cache all foreign key relationships by accessing them
|
|
53
53
|
for field in model_cls._meta.fields:
|
|
54
|
-
if
|
|
54
|
+
if (
|
|
55
|
+
field.is_relation
|
|
56
|
+
and not field.many_to_many
|
|
57
|
+
and not field.one_to_many
|
|
58
|
+
):
|
|
55
59
|
try:
|
|
56
60
|
# Access the related field to cache it before deletion
|
|
57
61
|
getattr(obj, field.name)
|
|
@@ -482,11 +486,19 @@ class HookQuerySetMixin:
|
|
|
482
486
|
passed through to the correct logic. For MTI, only a subset of options may be supported.
|
|
483
487
|
"""
|
|
484
488
|
model_cls = self.model
|
|
485
|
-
|
|
486
|
-
print(
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
489
|
+
|
|
490
|
+
print(
|
|
491
|
+
f"DEBUG: bulk_create called for {model_cls.__name__} with {len(objs)} objects"
|
|
492
|
+
)
|
|
493
|
+
print(
|
|
494
|
+
f"DEBUG: update_conflicts={update_conflicts}, unique_fields={unique_fields}, update_fields={update_fields}"
|
|
495
|
+
)
|
|
496
|
+
logger.debug(
|
|
497
|
+
f"bulk_create called for {model_cls.__name__} with {len(objs)} objects"
|
|
498
|
+
)
|
|
499
|
+
logger.debug(
|
|
500
|
+
f"update_conflicts={update_conflicts}, unique_fields={unique_fields}, update_fields={update_fields}"
|
|
501
|
+
)
|
|
490
502
|
|
|
491
503
|
# When you bulk insert you don't get the primary keys back (if it's an
|
|
492
504
|
# autoincrement, except if can_return_rows_from_bulk_insert=True), so
|
|
@@ -525,7 +537,7 @@ class HookQuerySetMixin:
|
|
|
525
537
|
# Fire hooks before DB ops
|
|
526
538
|
if not bypass_hooks:
|
|
527
539
|
ctx = HookContext(model_cls, bypass_hooks=False) # Pass bypass_hooks
|
|
528
|
-
|
|
540
|
+
|
|
529
541
|
if update_conflicts and unique_fields:
|
|
530
542
|
# For upsert operations, we need to determine which records will be created vs updated
|
|
531
543
|
# Check which records already exist in the database based on unique fields
|
|
@@ -535,7 +547,7 @@ class HookQuerySetMixin:
|
|
|
535
547
|
# Store the records for AFTER hooks to avoid duplicate queries
|
|
536
548
|
ctx.upsert_existing_records = existing_records
|
|
537
549
|
ctx.upsert_new_records = new_records
|
|
538
|
-
|
|
550
|
+
|
|
539
551
|
# Build a filter to check which records already exist
|
|
540
552
|
unique_values = []
|
|
541
553
|
for obj in objs:
|
|
@@ -549,6 +561,7 @@ class HookQuerySetMixin:
|
|
|
549
561
|
if unique_values:
|
|
550
562
|
# Query the database to see which records already exist - SINGLE BULK QUERY
|
|
551
563
|
from django.db.models import Q
|
|
564
|
+
|
|
552
565
|
existing_filters = Q()
|
|
553
566
|
for unique_value in unique_values:
|
|
554
567
|
filter_kwargs = {}
|
|
@@ -558,7 +571,9 @@ class HookQuerySetMixin:
|
|
|
558
571
|
|
|
559
572
|
# Get all existing records in one query and create a lookup set
|
|
560
573
|
existing_records_lookup = set()
|
|
561
|
-
for existing_record in model_cls.objects.filter(
|
|
574
|
+
for existing_record in model_cls.objects.filter(
|
|
575
|
+
existing_filters
|
|
576
|
+
).values_list(*unique_fields):
|
|
562
577
|
# Convert tuple to a hashable key for lookup
|
|
563
578
|
existing_records_lookup.add(existing_record)
|
|
564
579
|
|
|
@@ -572,7 +587,10 @@ class HookQuerySetMixin:
|
|
|
572
587
|
# Check if this record already exists using our bulk lookup
|
|
573
588
|
if obj_unique_value:
|
|
574
589
|
# Convert object values to tuple for comparison with existing records
|
|
575
|
-
obj_unique_tuple = tuple(
|
|
590
|
+
obj_unique_tuple = tuple(
|
|
591
|
+
obj_unique_value[field_name]
|
|
592
|
+
for field_name in unique_fields
|
|
593
|
+
)
|
|
576
594
|
if obj_unique_tuple in existing_records_lookup:
|
|
577
595
|
existing_records.append(obj)
|
|
578
596
|
else:
|
|
@@ -593,7 +611,7 @@ class HookQuerySetMixin:
|
|
|
593
611
|
elif hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
594
612
|
if getattr(obj, field.name) is None:
|
|
595
613
|
field.pre_save(obj, add=True)
|
|
596
|
-
|
|
614
|
+
|
|
597
615
|
# For existing records, preserve their original auto_now values
|
|
598
616
|
# We'll need to fetch them from the database to preserve the timestamps
|
|
599
617
|
if existing_records:
|
|
@@ -606,7 +624,7 @@ class HookQuerySetMixin:
|
|
|
606
624
|
unique_value[field_name] = getattr(obj, field_name)
|
|
607
625
|
if unique_value:
|
|
608
626
|
existing_unique_values.append(unique_value)
|
|
609
|
-
|
|
627
|
+
|
|
610
628
|
if existing_unique_values:
|
|
611
629
|
# Build filter to fetch existing records
|
|
612
630
|
existing_filters = Q()
|
|
@@ -615,14 +633,16 @@ class HookQuerySetMixin:
|
|
|
615
633
|
for field_name, value in unique_value.items():
|
|
616
634
|
filter_kwargs[field_name] = value
|
|
617
635
|
existing_filters |= Q(**filter_kwargs)
|
|
618
|
-
|
|
636
|
+
|
|
619
637
|
# Fetch existing records to preserve their auto_now values
|
|
620
638
|
existing_db_records = model_cls.objects.filter(existing_filters)
|
|
621
639
|
existing_db_map = {}
|
|
622
640
|
for db_record in existing_db_records:
|
|
623
|
-
key = tuple(
|
|
641
|
+
key = tuple(
|
|
642
|
+
getattr(db_record, field) for field in unique_fields
|
|
643
|
+
)
|
|
624
644
|
existing_db_map[key] = db_record
|
|
625
|
-
|
|
645
|
+
|
|
626
646
|
# For existing records, populate all fields from database and set auto_now fields
|
|
627
647
|
for obj in existing_records:
|
|
628
648
|
key = tuple(getattr(obj, field) for field in unique_fields)
|
|
@@ -631,28 +651,42 @@ class HookQuerySetMixin:
|
|
|
631
651
|
# Copy all fields from the database record to ensure completeness
|
|
632
652
|
populated_fields = []
|
|
633
653
|
for field in model_cls._meta.local_fields:
|
|
634
|
-
if field.name !=
|
|
654
|
+
if field.name != "id": # Don't overwrite the ID
|
|
635
655
|
db_value = getattr(db_record, field.name)
|
|
636
|
-
if
|
|
656
|
+
if (
|
|
657
|
+
db_value is not None
|
|
658
|
+
): # Only set non-None values
|
|
637
659
|
setattr(obj, field.name, db_value)
|
|
638
660
|
populated_fields.append(field.name)
|
|
639
|
-
print(
|
|
640
|
-
|
|
641
|
-
|
|
661
|
+
print(
|
|
662
|
+
f"DEBUG: Populated {len(populated_fields)} fields for existing record: {populated_fields}"
|
|
663
|
+
)
|
|
664
|
+
logger.debug(
|
|
665
|
+
f"Populated {len(populated_fields)} fields for existing record: {populated_fields}"
|
|
666
|
+
)
|
|
667
|
+
|
|
642
668
|
# Now set auto_now fields using Django's pre_save method
|
|
643
669
|
for field in model_cls._meta.local_fields:
|
|
644
670
|
if hasattr(field, "auto_now") and field.auto_now:
|
|
645
|
-
field.pre_save(
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
671
|
+
field.pre_save(
|
|
672
|
+
obj, add=False
|
|
673
|
+
) # add=False for updates
|
|
674
|
+
print(
|
|
675
|
+
f"DEBUG: Set {field.name} using pre_save for existing record {obj.pk}"
|
|
676
|
+
)
|
|
677
|
+
logger.debug(
|
|
678
|
+
f"Set {field.name} using pre_save for existing record {obj.pk}"
|
|
679
|
+
)
|
|
680
|
+
|
|
649
681
|
# Remove duplicate code since we're now handling this above
|
|
650
|
-
|
|
682
|
+
|
|
651
683
|
# CRITICAL: Handle auto_now fields intelligently for existing records
|
|
652
684
|
# We need to exclude them from Django's ON CONFLICT DO UPDATE clause to prevent
|
|
653
685
|
# Django's default behavior, but still ensure they get updated via pre_save
|
|
654
686
|
if existing_records and update_fields:
|
|
655
|
-
logger.debug(
|
|
687
|
+
logger.debug(
|
|
688
|
+
f"Processing {len(existing_records)} existing records with update_fields: {update_fields}"
|
|
689
|
+
)
|
|
656
690
|
|
|
657
691
|
# Identify auto_now fields
|
|
658
692
|
auto_now_fields = set()
|
|
@@ -669,24 +703,32 @@ class HookQuerySetMixin:
|
|
|
669
703
|
|
|
670
704
|
# Filter out auto_now fields from update_fields for the database operation
|
|
671
705
|
# This prevents Django from including them in ON CONFLICT DO UPDATE
|
|
672
|
-
filtered_update_fields = [
|
|
706
|
+
filtered_update_fields = [
|
|
707
|
+
f for f in update_fields if f not in auto_now_fields
|
|
708
|
+
]
|
|
673
709
|
|
|
674
|
-
logger.debug(
|
|
710
|
+
logger.debug(
|
|
711
|
+
f"Filtered update_fields: {filtered_update_fields}"
|
|
712
|
+
)
|
|
675
713
|
logger.debug(f"Excluded auto_now fields: {auto_now_fields}")
|
|
676
714
|
|
|
677
715
|
# Use filtered update_fields for Django's bulk_create operation
|
|
678
716
|
update_fields = filtered_update_fields
|
|
679
717
|
|
|
680
|
-
logger.debug(
|
|
718
|
+
logger.debug(
|
|
719
|
+
f"Final update_fields for DB operation: {update_fields}"
|
|
720
|
+
)
|
|
681
721
|
else:
|
|
682
722
|
logger.debug("No auto_now fields found to handle")
|
|
683
723
|
else:
|
|
684
|
-
logger.debug(
|
|
685
|
-
|
|
724
|
+
logger.debug(
|
|
725
|
+
f"No existing records or update_fields to process. existing_records: {len(existing_records) if existing_records else 0}, update_fields: {update_fields}"
|
|
726
|
+
)
|
|
727
|
+
|
|
686
728
|
# Run validation hooks on all records
|
|
687
729
|
if not bypass_validation:
|
|
688
730
|
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
689
|
-
|
|
731
|
+
|
|
690
732
|
# Run appropriate BEFORE hooks based on what will happen
|
|
691
733
|
if new_records:
|
|
692
734
|
engine.run(model_cls, BEFORE_CREATE, new_records, ctx=ctx)
|
|
@@ -702,7 +744,7 @@ class HookQuerySetMixin:
|
|
|
702
744
|
elif hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
703
745
|
if getattr(obj, field.name) is None:
|
|
704
746
|
field.pre_save(obj, add=True)
|
|
705
|
-
|
|
747
|
+
|
|
706
748
|
if not bypass_validation:
|
|
707
749
|
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
708
750
|
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
@@ -731,10 +773,16 @@ class HookQuerySetMixin:
|
|
|
731
773
|
# but we need to call it on the base manager to avoid recursion
|
|
732
774
|
# Filter out custom parameters that Django's bulk_create doesn't accept
|
|
733
775
|
|
|
734
|
-
logger.debug(
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
776
|
+
logger.debug(
|
|
777
|
+
f"Calling Django bulk_create with update_fields: {update_fields}"
|
|
778
|
+
)
|
|
779
|
+
logger.debug(
|
|
780
|
+
f"Calling Django bulk_create with update_conflicts: {update_conflicts}"
|
|
781
|
+
)
|
|
782
|
+
logger.debug(
|
|
783
|
+
f"Calling Django bulk_create with unique_fields: {unique_fields}"
|
|
784
|
+
)
|
|
785
|
+
|
|
738
786
|
result = super().bulk_create(
|
|
739
787
|
objs,
|
|
740
788
|
batch_size=batch_size,
|
|
@@ -743,15 +791,17 @@ class HookQuerySetMixin:
|
|
|
743
791
|
update_fields=update_fields,
|
|
744
792
|
unique_fields=unique_fields,
|
|
745
793
|
)
|
|
746
|
-
|
|
794
|
+
|
|
747
795
|
logger.debug(f"Django bulk_create completed with result: {result}")
|
|
748
796
|
|
|
749
797
|
# Fire AFTER hooks
|
|
750
798
|
if not bypass_hooks:
|
|
751
799
|
if update_conflicts and unique_fields:
|
|
752
800
|
# Handle auto_now fields that were excluded from the main update
|
|
753
|
-
if hasattr(ctx,
|
|
754
|
-
logger.debug(
|
|
801
|
+
if hasattr(ctx, "auto_now_fields") and existing_records:
|
|
802
|
+
logger.debug(
|
|
803
|
+
f"Performing separate update for auto_now fields: {ctx.auto_now_fields}"
|
|
804
|
+
)
|
|
755
805
|
|
|
756
806
|
# Perform a separate bulk_update for the auto_now fields that were set via pre_save
|
|
757
807
|
# This ensures they get saved to the database even though they were excluded from the main upsert
|
|
@@ -761,29 +811,39 @@ class HookQuerySetMixin:
|
|
|
761
811
|
auto_now_update_result = base_manager.bulk_update(
|
|
762
812
|
existing_records, list(ctx.auto_now_fields)
|
|
763
813
|
)
|
|
764
|
-
logger.debug(
|
|
814
|
+
logger.debug(
|
|
815
|
+
f"Auto_now fields update completed with result: {auto_now_update_result}"
|
|
816
|
+
)
|
|
765
817
|
except Exception as e:
|
|
766
818
|
logger.error(f"Failed to update auto_now fields: {e}")
|
|
767
819
|
# Don't raise the exception - the main operation succeeded
|
|
768
820
|
|
|
769
821
|
# Restore original update_fields if we modified them
|
|
770
|
-
if hasattr(ctx,
|
|
771
|
-
logger.debug(
|
|
822
|
+
if hasattr(ctx, "original_update_fields"):
|
|
823
|
+
logger.debug(
|
|
824
|
+
f"Restoring original update_fields: {ctx.original_update_fields}"
|
|
825
|
+
)
|
|
772
826
|
update_fields = ctx.original_update_fields
|
|
773
|
-
delattr(ctx,
|
|
774
|
-
if hasattr(ctx,
|
|
775
|
-
delattr(ctx,
|
|
827
|
+
delattr(ctx, "original_update_fields")
|
|
828
|
+
if hasattr(ctx, "auto_now_fields"):
|
|
829
|
+
delattr(ctx, "auto_now_fields")
|
|
776
830
|
logger.debug(f"Restored update_fields: {update_fields}")
|
|
777
831
|
|
|
778
832
|
# For upsert operations, reuse the existing/new records determination from BEFORE hooks
|
|
779
833
|
# This avoids duplicate queries and improves performance
|
|
780
|
-
if hasattr(ctx,
|
|
834
|
+
if hasattr(ctx, "upsert_existing_records") and hasattr(
|
|
835
|
+
ctx, "upsert_new_records"
|
|
836
|
+
):
|
|
781
837
|
existing_records = ctx.upsert_existing_records
|
|
782
838
|
new_records = ctx.upsert_new_records
|
|
783
|
-
logger.debug(
|
|
839
|
+
logger.debug(
|
|
840
|
+
f"Reusing upsert record classification from BEFORE hooks: {len(existing_records)} existing, {len(new_records)} new"
|
|
841
|
+
)
|
|
784
842
|
else:
|
|
785
843
|
# Fallback: determine records that actually exist after bulk operation
|
|
786
|
-
logger.warning(
|
|
844
|
+
logger.warning(
|
|
845
|
+
"Upsert record classification not found in context, performing fallback query"
|
|
846
|
+
)
|
|
787
847
|
existing_records = []
|
|
788
848
|
new_records = []
|
|
789
849
|
|
|
@@ -800,6 +860,7 @@ class HookQuerySetMixin:
|
|
|
800
860
|
if unique_values:
|
|
801
861
|
# Query the database to see which records exist after bulk operation
|
|
802
862
|
from django.db.models import Q
|
|
863
|
+
|
|
803
864
|
existing_filters = Q()
|
|
804
865
|
for unique_value in unique_values:
|
|
805
866
|
filter_kwargs = {}
|
|
@@ -809,7 +870,9 @@ class HookQuerySetMixin:
|
|
|
809
870
|
|
|
810
871
|
# Get all existing records in one query and create a lookup set
|
|
811
872
|
existing_records_lookup = set()
|
|
812
|
-
for existing_record in model_cls.objects.filter(
|
|
873
|
+
for existing_record in model_cls.objects.filter(
|
|
874
|
+
existing_filters
|
|
875
|
+
).values_list(*unique_fields):
|
|
813
876
|
# Convert tuple to a hashable key for lookup
|
|
814
877
|
existing_records_lookup.add(existing_record)
|
|
815
878
|
|
|
@@ -818,12 +881,17 @@ class HookQuerySetMixin:
|
|
|
818
881
|
obj_unique_value = {}
|
|
819
882
|
for field_name in unique_fields:
|
|
820
883
|
if hasattr(obj, field_name):
|
|
821
|
-
obj_unique_value[field_name] = getattr(
|
|
884
|
+
obj_unique_value[field_name] = getattr(
|
|
885
|
+
obj, field_name
|
|
886
|
+
)
|
|
822
887
|
|
|
823
888
|
# Check if this record exists using our bulk lookup
|
|
824
889
|
if obj_unique_value:
|
|
825
890
|
# Convert object values to tuple for comparison with existing records
|
|
826
|
-
obj_unique_tuple = tuple(
|
|
891
|
+
obj_unique_tuple = tuple(
|
|
892
|
+
obj_unique_value[field_name]
|
|
893
|
+
for field_name in unique_fields
|
|
894
|
+
)
|
|
827
895
|
if obj_unique_tuple in existing_records_lookup:
|
|
828
896
|
existing_records.append(obj)
|
|
829
897
|
else:
|
|
@@ -860,12 +928,12 @@ class HookQuerySetMixin:
|
|
|
860
928
|
# Get primary key field names
|
|
861
929
|
pk_fields = [f.name for f in model_cls._meta.pk_fields]
|
|
862
930
|
if not pk_fields:
|
|
863
|
-
pk_fields = [
|
|
931
|
+
pk_fields = ["pk"]
|
|
864
932
|
|
|
865
933
|
# Get all object PKs
|
|
866
934
|
obj_pks = []
|
|
867
935
|
for obj in objs:
|
|
868
|
-
if hasattr(obj,
|
|
936
|
+
if hasattr(obj, "pk") and obj.pk is not None:
|
|
869
937
|
obj_pks.append(obj.pk)
|
|
870
938
|
else:
|
|
871
939
|
# Skip objects without PKs
|
|
@@ -875,7 +943,9 @@ class HookQuerySetMixin:
|
|
|
875
943
|
return set()
|
|
876
944
|
|
|
877
945
|
# Fetch current database values for all objects
|
|
878
|
-
existing_objs = {
|
|
946
|
+
existing_objs = {
|
|
947
|
+
obj.pk: obj for obj in model_cls.objects.filter(pk__in=obj_pks)
|
|
948
|
+
}
|
|
879
949
|
|
|
880
950
|
# Compare each object's current values with database values
|
|
881
951
|
for obj in objs:
|
|
@@ -904,9 +974,7 @@ class HookQuerySetMixin:
|
|
|
904
974
|
return changed_fields
|
|
905
975
|
|
|
906
976
|
@transaction.atomic
|
|
907
|
-
def bulk_update(
|
|
908
|
-
self, objs, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
909
|
-
):
|
|
977
|
+
def bulk_update(self, objs, bypass_hooks=False, bypass_validation=False, **kwargs):
|
|
910
978
|
"""
|
|
911
979
|
Bulk update objects in the database with MTI support.
|
|
912
980
|
Automatically detects which fields have changed by comparing with database values.
|
|
@@ -928,7 +996,9 @@ class HookQuerySetMixin:
|
|
|
928
996
|
logger.debug(
|
|
929
997
|
f"bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)} changed_fields={changed_fields}"
|
|
930
998
|
)
|
|
931
|
-
print(
|
|
999
|
+
print(
|
|
1000
|
+
f"DEBUG: bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)} changed_fields={changed_fields}"
|
|
1001
|
+
)
|
|
932
1002
|
|
|
933
1003
|
# Check for MTI
|
|
934
1004
|
is_mti = False
|
|
@@ -954,7 +1024,9 @@ class HookQuerySetMixin:
|
|
|
954
1024
|
pk_field_names = [f.name for f in pk_fields]
|
|
955
1025
|
auto_now_fields = []
|
|
956
1026
|
custom_update_fields = [] # Fields that need pre_save() called on update
|
|
957
|
-
logger.debug(
|
|
1027
|
+
logger.debug(
|
|
1028
|
+
f"Checking for auto_now and custom update fields in {model_cls.__name__}"
|
|
1029
|
+
)
|
|
958
1030
|
for field in model_cls._meta.local_concrete_fields:
|
|
959
1031
|
# Only add auto_now fields (like updated_at) that aren't already in the fields list
|
|
960
1032
|
# Don't include auto_now_add fields (like created_at) as they should only be set on creation
|
|
@@ -969,36 +1041,51 @@ class HookQuerySetMixin:
|
|
|
969
1041
|
logger.debug(f"Added auto_now field {field.name} to fields list")
|
|
970
1042
|
print(f"DEBUG: Added auto_now field {field.name} to fields list")
|
|
971
1043
|
else:
|
|
972
|
-
logger.debug(
|
|
973
|
-
|
|
1044
|
+
logger.debug(
|
|
1045
|
+
f"Auto_now field {field.name} already in fields list or is PK"
|
|
1046
|
+
)
|
|
1047
|
+
print(
|
|
1048
|
+
f"DEBUG: Auto_now field {field.name} already in fields list or is PK"
|
|
1049
|
+
)
|
|
974
1050
|
elif hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
975
1051
|
logger.debug(f"Found auto_now_add field: {field.name} (skipping)")
|
|
976
1052
|
# Check for custom fields that might need pre_save() on update (like CurrentUserField)
|
|
977
|
-
elif hasattr(field,
|
|
1053
|
+
elif hasattr(field, "pre_save"):
|
|
978
1054
|
# Only call pre_save on fields that aren't already being updated
|
|
979
1055
|
if field.name not in fields_set and field.name not in pk_field_names:
|
|
980
1056
|
custom_update_fields.append(field)
|
|
981
1057
|
logger.debug(f"Found custom field with pre_save: {field.name}")
|
|
982
1058
|
print(f"DEBUG: Found custom field with pre_save: {field.name}")
|
|
983
|
-
|
|
1059
|
+
|
|
984
1060
|
logger.debug(f"Auto_now fields detected: {auto_now_fields}")
|
|
985
1061
|
print(f"DEBUG: Auto_now fields detected: {auto_now_fields}")
|
|
986
|
-
|
|
1062
|
+
|
|
987
1063
|
# Set auto_now field values to current timestamp
|
|
988
1064
|
if auto_now_fields:
|
|
989
1065
|
from django.utils import timezone
|
|
1066
|
+
|
|
990
1067
|
current_time = timezone.now()
|
|
991
|
-
print(
|
|
992
|
-
|
|
1068
|
+
print(
|
|
1069
|
+
f"DEBUG: Setting auto_now fields {auto_now_fields} to current time: {current_time}"
|
|
1070
|
+
)
|
|
1071
|
+
logger.debug(
|
|
1072
|
+
f"Setting auto_now fields {auto_now_fields} to current time: {current_time}"
|
|
1073
|
+
)
|
|
993
1074
|
for obj in objs:
|
|
994
1075
|
for field_name in auto_now_fields:
|
|
995
1076
|
setattr(obj, field_name, current_time)
|
|
996
|
-
print(
|
|
1077
|
+
print(
|
|
1078
|
+
f"DEBUG: Set {field_name} to {current_time} for object {obj.pk}"
|
|
1079
|
+
)
|
|
997
1080
|
|
|
998
1081
|
# Call pre_save() on custom fields that need update handling
|
|
999
1082
|
if custom_update_fields:
|
|
1000
|
-
logger.debug(
|
|
1001
|
-
|
|
1083
|
+
logger.debug(
|
|
1084
|
+
f"Calling pre_save() on custom update fields: {[f.name for f in custom_update_fields]}"
|
|
1085
|
+
)
|
|
1086
|
+
print(
|
|
1087
|
+
f"DEBUG: Calling pre_save() on custom update fields: {[f.name for f in custom_update_fields]}"
|
|
1088
|
+
)
|
|
1002
1089
|
for obj in objs:
|
|
1003
1090
|
for field in custom_update_fields:
|
|
1004
1091
|
try:
|
|
@@ -1008,29 +1095,63 @@ class HookQuerySetMixin:
|
|
|
1008
1095
|
if new_value is not None:
|
|
1009
1096
|
setattr(obj, field.name, new_value)
|
|
1010
1097
|
# Add this field to the update fields if it's not already there and not a primary key
|
|
1011
|
-
if
|
|
1098
|
+
if (
|
|
1099
|
+
field.name not in fields_set
|
|
1100
|
+
and field.name not in pk_field_names
|
|
1101
|
+
):
|
|
1012
1102
|
fields_set.add(field.name)
|
|
1013
|
-
logger.debug(
|
|
1014
|
-
|
|
1103
|
+
logger.debug(
|
|
1104
|
+
f"Custom field {field.name} updated via pre_save() for object {obj.pk}"
|
|
1105
|
+
)
|
|
1106
|
+
print(
|
|
1107
|
+
f"DEBUG: Custom field {field.name} updated via pre_save() for object {obj.pk}"
|
|
1108
|
+
)
|
|
1015
1109
|
except Exception as e:
|
|
1016
|
-
logger.warning(
|
|
1017
|
-
|
|
1110
|
+
logger.warning(
|
|
1111
|
+
f"Failed to call pre_save() on custom field {field.name}: {e}"
|
|
1112
|
+
)
|
|
1113
|
+
print(
|
|
1114
|
+
f"DEBUG: Failed to call pre_save() on custom field {field.name}: {e}"
|
|
1115
|
+
)
|
|
1018
1116
|
|
|
1019
1117
|
# Handle MTI models differently
|
|
1020
1118
|
if is_mti:
|
|
1021
1119
|
result = self._mti_bulk_update(objs, list(fields_set), **kwargs)
|
|
1022
1120
|
else:
|
|
1023
1121
|
# For single-table models, use Django's built-in bulk_update
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1122
|
+
# Filter out parameters that are not supported by Django's bulk_update
|
|
1123
|
+
unsupported_params = ["unique_fields", "update_conflicts", "update_fields", "ignore_conflicts"]
|
|
1124
|
+
django_kwargs = {}
|
|
1125
|
+
|
|
1126
|
+
# Check if all objects have primary keys set before proceeding
|
|
1127
|
+
if not all(obj._is_pk_set() for obj in objs):
|
|
1128
|
+
missing_pk_count = sum(1 for obj in objs if not obj._is_pk_set())
|
|
1129
|
+
logger.error(
|
|
1130
|
+
f"bulk_update failed: {missing_pk_count} out of {len(objs)} objects don't have primary keys set. "
|
|
1131
|
+
"All objects must be saved to the database before bulk_update can be used."
|
|
1132
|
+
)
|
|
1133
|
+
print(f"ERROR: {missing_pk_count} objects don't have primary keys set")
|
|
1134
|
+
raise ValueError(
|
|
1135
|
+
f"All bulk_update() objects must have a primary key set. "
|
|
1136
|
+
f"{missing_pk_count} out of {len(objs)} objects are missing primary keys."
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
for k, v in kwargs.items():
|
|
1140
|
+
if k in unsupported_params:
|
|
1141
|
+
logger.warning(
|
|
1142
|
+
f"Parameter '{k}' is not supported by bulk_update. "
|
|
1143
|
+
f"This parameter is only available in bulk_create for UPSERT operations."
|
|
1144
|
+
)
|
|
1145
|
+
print(f"WARNING: Parameter '{k}' is not supported by bulk_update")
|
|
1146
|
+
elif k not in ["bypass_hooks", "bypass_validation"]:
|
|
1147
|
+
django_kwargs[k] = v
|
|
1029
1148
|
logger.debug("Calling Django bulk_update")
|
|
1030
1149
|
print("DEBUG: Calling Django bulk_update")
|
|
1031
1150
|
# Build a per-object concrete value map to avoid leaking expressions into hooks
|
|
1032
1151
|
value_map = {}
|
|
1033
|
-
logger.debug(
|
|
1152
|
+
logger.debug(
|
|
1153
|
+
f"Building value map for {len(objs)} objects with fields: {list(fields_set)}"
|
|
1154
|
+
)
|
|
1034
1155
|
for obj in objs:
|
|
1035
1156
|
if obj.pk is None:
|
|
1036
1157
|
continue
|
|
@@ -1039,7 +1160,9 @@ class HookQuerySetMixin:
|
|
|
1039
1160
|
# Capture raw values assigned on the object (not expressions)
|
|
1040
1161
|
field_values[field_name] = getattr(obj, field_name)
|
|
1041
1162
|
if field_name in auto_now_fields:
|
|
1042
|
-
logger.debug(
|
|
1163
|
+
logger.debug(
|
|
1164
|
+
f"Object {obj.pk} {field_name}: {field_values[field_name]}"
|
|
1165
|
+
)
|
|
1043
1166
|
if field_values:
|
|
1044
1167
|
value_map[obj.pk] = field_values
|
|
1045
1168
|
|
|
@@ -1370,7 +1493,9 @@ class HookQuerySetMixin:
|
|
|
1370
1493
|
|
|
1371
1494
|
return child_obj
|
|
1372
1495
|
|
|
1373
|
-
def _mti_bulk_update(
|
|
1496
|
+
def _mti_bulk_update(
|
|
1497
|
+
self, objs, fields, field_groups=None, inheritance_chain=None, **kwargs
|
|
1498
|
+
):
|
|
1374
1499
|
"""
|
|
1375
1500
|
Custom bulk update implementation for MTI models.
|
|
1376
1501
|
Updates each table in the inheritance chain efficiently using Django's batch_size.
|
|
@@ -1379,12 +1504,31 @@ class HookQuerySetMixin:
|
|
|
1379
1504
|
if inheritance_chain is None:
|
|
1380
1505
|
inheritance_chain = self._get_inheritance_chain()
|
|
1381
1506
|
|
|
1382
|
-
#
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1507
|
+
# Check if all objects have primary keys set before proceeding
|
|
1508
|
+
if not all(obj._is_pk_set() for obj in objs):
|
|
1509
|
+
missing_pk_count = sum(1 for obj in objs if not obj._is_pk_set())
|
|
1510
|
+
logger.error(
|
|
1511
|
+
f"MTI bulk_update failed: {missing_pk_count} out of {len(objs)} objects don't have primary keys set. "
|
|
1512
|
+
"All objects must be saved to the database before bulk_update can be used."
|
|
1513
|
+
)
|
|
1514
|
+
print(f"ERROR: {missing_pk_count} objects don't have primary keys set")
|
|
1515
|
+
raise ValueError(
|
|
1516
|
+
f"All bulk_update() objects must have a primary key set. "
|
|
1517
|
+
f"{missing_pk_count} out of {len(objs)} objects are missing primary keys."
|
|
1518
|
+
)
|
|
1519
|
+
|
|
1520
|
+
# Remove custom hook kwargs and unsupported parameters before passing to Django internals
|
|
1521
|
+
unsupported_params = ["unique_fields", "update_conflicts", "update_fields", "ignore_conflicts"]
|
|
1522
|
+
django_kwargs = {}
|
|
1523
|
+
for k, v in kwargs.items():
|
|
1524
|
+
if k in unsupported_params:
|
|
1525
|
+
logger.warning(
|
|
1526
|
+
f"Parameter '{k}' is not supported by bulk_update. "
|
|
1527
|
+
f"This parameter is only available in bulk_create for UPSERT operations."
|
|
1528
|
+
)
|
|
1529
|
+
print(f"WARNING: Parameter '{k}' is not supported by bulk_update")
|
|
1530
|
+
elif k not in ["bypass_hooks", "bypass_validation"]:
|
|
1531
|
+
django_kwargs[k] = v
|
|
1388
1532
|
|
|
1389
1533
|
# Safety check to prevent infinite recursion
|
|
1390
1534
|
if len(inheritance_chain) > 10: # Arbitrary limit to prevent infinite loops
|
|
@@ -1401,15 +1545,19 @@ class HookQuerySetMixin:
|
|
|
1401
1545
|
if hasattr(field, "auto_now") and field.auto_now:
|
|
1402
1546
|
field.pre_save(obj, add=False)
|
|
1403
1547
|
# Check for custom fields that might need pre_save() on update (like CurrentUserField)
|
|
1404
|
-
elif hasattr(field,
|
|
1548
|
+
elif hasattr(field, "pre_save") and field.name not in fields:
|
|
1405
1549
|
try:
|
|
1406
1550
|
new_value = field.pre_save(obj, add=False)
|
|
1407
1551
|
if new_value is not None:
|
|
1408
1552
|
setattr(obj, field.name, new_value)
|
|
1409
1553
|
custom_update_fields.append(field.name)
|
|
1410
|
-
logger.debug(
|
|
1554
|
+
logger.debug(
|
|
1555
|
+
f"Custom field {field.name} updated via pre_save() for MTI object {obj.pk}"
|
|
1556
|
+
)
|
|
1411
1557
|
except Exception as e:
|
|
1412
|
-
logger.warning(
|
|
1558
|
+
logger.warning(
|
|
1559
|
+
f"Failed to call pre_save() on custom field {field.name} in MTI: {e}"
|
|
1560
|
+
)
|
|
1413
1561
|
|
|
1414
1562
|
# Add auto_now fields to the fields list so they get updated in the database
|
|
1415
1563
|
auto_now_fields = set()
|
|
@@ -1587,7 +1735,11 @@ class HookQuerySetMixin:
|
|
|
1587
1735
|
if obj.pk is not None:
|
|
1588
1736
|
# Cache all foreign key relationships by accessing them
|
|
1589
1737
|
for field in model_cls._meta.fields:
|
|
1590
|
-
if
|
|
1738
|
+
if (
|
|
1739
|
+
field.is_relation
|
|
1740
|
+
and not field.many_to_many
|
|
1741
|
+
and not field.one_to_many
|
|
1742
|
+
):
|
|
1591
1743
|
try:
|
|
1592
1744
|
# Access the related field to cache it before deletion
|
|
1593
1745
|
getattr(obj, field.name)
|
|
@@ -6,12 +6,12 @@ django_bulk_hooks/decorators.py,sha256=k70-BzWwS3wZu_uph5B5qXd6YpwXLQ9hMpOzPUy6i
|
|
|
6
6
|
django_bulk_hooks/engine.py,sha256=M3b7Rcb65PYAZTLfWrIRi99BUBPgSLCryL3MSjMVlfQ,2663
|
|
7
7
|
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
8
8
|
django_bulk_hooks/handler.py,sha256=Bx-W6yyiciKMyy-BRxUt3CmRPCrX9_LhQgU-5LaJTjg,6019
|
|
9
|
-
django_bulk_hooks/manager.py,sha256=
|
|
9
|
+
django_bulk_hooks/manager.py,sha256=mk2RYm-iBk7xYTcYfuo7XPHJbPP5TCTns5IxJMwRY2M,3867
|
|
10
10
|
django_bulk_hooks/models.py,sha256=WtSfc4GBOG_oOt8n37cVvid0MtFIGze9JYKSixil2y0,4370
|
|
11
11
|
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
-
django_bulk_hooks/queryset.py,sha256=
|
|
12
|
+
django_bulk_hooks/queryset.py,sha256=hq2PHSFM3N0GBFXekYT6FGFHHM85uoJWpQVj4vnDG_s,84452
|
|
13
13
|
django_bulk_hooks/registry.py,sha256=GRUTGVQEO2sdkC9OaZ9Q3U7mM-3Ix83uTyvrlTtpatw,1317
|
|
14
|
-
django_bulk_hooks-0.1.
|
|
15
|
-
django_bulk_hooks-0.1.
|
|
16
|
-
django_bulk_hooks-0.1.
|
|
17
|
-
django_bulk_hooks-0.1.
|
|
14
|
+
django_bulk_hooks-0.1.266.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.266.dist-info/METADATA,sha256=HjwwuWICGq1svF5aeNWtksQLwIkuenJdRC9N2PyT5ds,9115
|
|
16
|
+
django_bulk_hooks-0.1.266.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
17
|
+
django_bulk_hooks-0.1.266.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|