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.

@@ -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,
@@ -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 field.is_relation and not field.many_to_many and not field.one_to_many:
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(f"DEBUG: bulk_create called for {model_cls.__name__} with {len(objs)} objects")
487
- print(f"DEBUG: update_conflicts={update_conflicts}, unique_fields={unique_fields}, update_fields={update_fields}")
488
- logger.debug(f"bulk_create called for {model_cls.__name__} with {len(objs)} objects")
489
- logger.debug(f"update_conflicts={update_conflicts}, unique_fields={unique_fields}, update_fields={update_fields}")
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(existing_filters).values_list(*unique_fields):
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(obj_unique_value[field_name] for field_name in unique_fields)
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(getattr(db_record, field) for field in unique_fields)
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 != 'id': # Don't overwrite the ID
654
+ if field.name != "id": # Don't overwrite the ID
635
655
  db_value = getattr(db_record, field.name)
636
- if db_value is not None: # Only set non-None values
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(f"DEBUG: Populated {len(populated_fields)} fields for existing record: {populated_fields}")
640
- logger.debug(f"Populated {len(populated_fields)} fields for existing record: {populated_fields}")
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(obj, add=False) # add=False for updates
646
- print(f"DEBUG: Set {field.name} using pre_save for existing record {obj.pk}")
647
- logger.debug(f"Set {field.name} using pre_save for existing record {obj.pk}")
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(f"Processing {len(existing_records)} existing records with update_fields: {update_fields}")
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 = [f for f in update_fields if f not in auto_now_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(f"Filtered update_fields: {filtered_update_fields}")
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(f"Final update_fields for DB operation: {update_fields}")
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(f"No existing records or update_fields to process. existing_records: {len(existing_records) if existing_records else 0}, update_fields: {update_fields}")
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(f"Calling Django bulk_create with update_fields: {update_fields}")
735
- logger.debug(f"Calling Django bulk_create with update_conflicts: {update_conflicts}")
736
- logger.debug(f"Calling Django bulk_create with unique_fields: {unique_fields}")
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, 'auto_now_fields') and existing_records:
754
- logger.debug(f"Performing separate update for auto_now fields: {ctx.auto_now_fields}")
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(f"Auto_now fields update completed with result: {auto_now_update_result}")
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, 'original_update_fields'):
771
- logger.debug(f"Restoring original update_fields: {ctx.original_update_fields}")
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, 'original_update_fields')
774
- if hasattr(ctx, 'auto_now_fields'):
775
- delattr(ctx, 'auto_now_fields')
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, 'upsert_existing_records') and hasattr(ctx, 'upsert_new_records'):
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(f"Reusing upsert record classification from BEFORE hooks: {len(existing_records)} existing, {len(new_records)} new")
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("Upsert record classification not found in context, performing fallback query")
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(existing_filters).values_list(*unique_fields):
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(obj, field_name)
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(obj_unique_value[field_name] for field_name in unique_fields)
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 = ['pk']
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, 'pk') and obj.pk is not None:
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 = {obj.pk: obj for obj in model_cls.objects.filter(pk__in=obj_pks)}
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(f"DEBUG: bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)} changed_fields={changed_fields}")
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(f"Checking for auto_now and custom update fields in {model_cls.__name__}")
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(f"Auto_now field {field.name} already in fields list or is PK")
973
- print(f"DEBUG: Auto_now field {field.name} already in fields list or is PK")
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, 'pre_save'):
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(f"DEBUG: Setting auto_now fields {auto_now_fields} to current time: {current_time}")
992
- logger.debug(f"Setting auto_now fields {auto_now_fields} to current time: {current_time}")
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(f"DEBUG: Set {field_name} to {current_time} for object {obj.pk}")
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(f"Calling pre_save() on custom update fields: {[f.name for f in custom_update_fields]}")
1001
- print(f"DEBUG: Calling pre_save() on custom update fields: {[f.name for f in custom_update_fields]}")
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 field.name not in fields_set and field.name not in pk_field_names:
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(f"Custom field {field.name} updated via pre_save() for object {obj.pk}")
1014
- print(f"DEBUG: Custom field {field.name} updated via pre_save() for object {obj.pk}")
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(f"Failed to call pre_save() on custom field {field.name}: {e}")
1017
- print(f"DEBUG: Failed to call pre_save() on custom field {field.name}: {e}")
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
- django_kwargs = {
1025
- k: v
1026
- for k, v in kwargs.items()
1027
- if k not in ["bypass_hooks", "bypass_validation"]
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(f"Building value map for {len(objs)} objects with fields: {list(fields_set)}")
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(f"Object {obj.pk} {field_name}: {field_values[field_name]}")
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(self, objs, fields, field_groups=None, inheritance_chain=None, **kwargs):
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
- # Remove custom hook kwargs before passing to Django internals
1383
- django_kwargs = {
1384
- k: v
1385
- for k, v in kwargs.items()
1386
- if k not in ["bypass_hooks", "bypass_validation"]
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, 'pre_save') and field.name not in fields:
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(f"Custom field {field.name} updated via pre_save() for MTI object {obj.pk}")
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(f"Failed to call pre_save() on custom field {field.name} in MTI: {e}")
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 field.is_relation and not field.many_to_many and not field.one_to_many:
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.1.264
3
+ Version: 0.1.266
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
@@ -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=QJCrP5dM7f6aJaoRtFi1EXAzAtnwvDdRvjARZ3t0mbc,3604
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=qDbN8HqA_1UDVcxZJHNjr_OGYGpBUj4IyKKgORl0-64,79272
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.264.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
15
- django_bulk_hooks-0.1.264.dist-info/METADATA,sha256=ys55sFNDkhz246D5bg0DILNQ2jWiTLx86HphXAGcx5s,9115
16
- django_bulk_hooks-0.1.264.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
17
- django_bulk_hooks-0.1.264.dist-info/RECORD,,
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,,