django-bulk-hooks 0.2.46__tar.gz → 0.2.47__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.

Files changed (26) hide show
  1. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/PKG-INFO +1 -1
  2. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/operations/bulk_executor.py +8 -1
  3. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/operations/coordinator.py +9 -1
  4. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/operations/mti_handler.py +29 -0
  5. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/operations/record_classifier.py +8 -4
  6. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/pyproject.toml +1 -1
  7. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/LICENSE +0 -0
  8. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/README.md +0 -0
  9. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/__init__.py +0 -0
  10. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/changeset.py +0 -0
  11. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/conditions.py +0 -0
  12. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/constants.py +0 -0
  13. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/context.py +0 -0
  14. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/decorators.py +0 -0
  15. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/dispatcher.py +0 -0
  16. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/enums.py +0 -0
  17. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/factory.py +0 -0
  18. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/handler.py +0 -0
  19. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/helpers.py +0 -0
  20. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/manager.py +0 -0
  21. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/models.py +0 -0
  22. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/operations/__init__.py +0 -0
  23. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/operations/analyzer.py +0 -0
  24. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/operations/mti_plans.py +0 -0
  25. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/queryset.py +0 -0
  26. {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.47}/django_bulk_hooks/registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: django-bulk-hooks
3
- Version: 0.2.46
3
+ Version: 0.2.47
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
@@ -80,7 +80,14 @@ class BulkExecutor:
80
80
  existing_record_ids = set()
81
81
  existing_pks_map = {}
82
82
  if update_conflicts and unique_fields:
83
- existing_record_ids, existing_pks_map = self.record_classifier.classify_for_upsert(objs, unique_fields)
83
+ # For MTI, find which model has the unique fields and query THAT model
84
+ # This handles the schema migration case where parent exists but child doesn't
85
+ query_model = self.mti_handler.find_model_with_unique_fields(unique_fields)
86
+ logger.info(f"MTI upsert: querying {query_model.__name__} for unique fields {unique_fields}")
87
+
88
+ existing_record_ids, existing_pks_map = self.record_classifier.classify_for_upsert(
89
+ objs, unique_fields, query_model=query_model
90
+ )
84
91
  logger.info(f"MTI Upsert classification: {len(existing_record_ids)} existing, {len(objs) - len(existing_record_ids)} new")
85
92
  logger.info(f"existing_record_ids: {existing_record_ids}")
86
93
  logger.info(f"existing_pks_map: {existing_pks_map}")
@@ -136,7 +136,15 @@ class BulkOperationCoordinator:
136
136
  existing_record_ids = set()
137
137
  existing_pks_map = {}
138
138
  if update_conflicts and unique_fields:
139
- existing_record_ids, existing_pks_map = self.record_classifier.classify_for_upsert(objs, unique_fields)
139
+ # For MTI models, query the parent model that has the unique fields
140
+ query_model = None
141
+ if self.mti_handler.is_mti_model():
142
+ query_model = self.mti_handler.find_model_with_unique_fields(unique_fields)
143
+ logger.info(f"MTI model detected: querying {query_model.__name__} for unique fields {unique_fields}")
144
+
145
+ existing_record_ids, existing_pks_map = self.record_classifier.classify_for_upsert(
146
+ objs, unique_fields, query_model=query_model
147
+ )
140
148
  logger.info(f"Upsert operation: {len(existing_record_ids)} existing, {len(objs) - len(existing_record_ids)} new records")
141
149
  logger.debug(f"Existing record IDs: {existing_record_ids}")
142
150
  logger.debug(f"Existing PKs map: {existing_pks_map}")
@@ -111,6 +111,35 @@ class MTIHandler:
111
111
  """
112
112
  return list(model_cls._meta.local_fields)
113
113
 
114
+ def find_model_with_unique_fields(self, unique_fields):
115
+ """
116
+ Find which model in the inheritance chain contains the unique fields.
117
+
118
+ This is critical for MTI upserts: we need to query the model that has
119
+ the unique constraint, not necessarily the child model.
120
+
121
+ Args:
122
+ unique_fields: List of field names forming the unique constraint
123
+
124
+ Returns:
125
+ Model class that contains all the unique fields (closest to root)
126
+ """
127
+ if not unique_fields:
128
+ return self.model_cls
129
+
130
+ inheritance_chain = self.get_inheritance_chain()
131
+
132
+ # Start from root and find the first model that has all unique fields
133
+ for model_cls in inheritance_chain:
134
+ model_field_names = {f.name for f in model_cls._meta.local_fields}
135
+
136
+ # Check if this model has all the unique fields
137
+ if all(field in model_field_names for field in unique_fields):
138
+ return model_cls
139
+
140
+ # Fallback to child model (shouldn't happen if unique_fields are valid)
141
+ return self.model_cls
142
+
114
143
  # ==================== MTI BULK CREATE PLANNING ====================
115
144
 
116
145
  def build_create_plan(
@@ -31,7 +31,7 @@ class RecordClassifier:
31
31
  """
32
32
  self.model_cls = model_cls
33
33
 
34
- def classify_for_upsert(self, objs, unique_fields):
34
+ def classify_for_upsert(self, objs, unique_fields, query_model=None):
35
35
  """
36
36
  Classify records as new or existing based on unique_fields.
37
37
 
@@ -41,6 +41,7 @@ class RecordClassifier:
41
41
  Args:
42
42
  objs: List of model instances
43
43
  unique_fields: List of field names that form the unique constraint
44
+ query_model: Optional model class to query (for MTI, may be different from self.model_cls)
44
45
 
45
46
  Returns:
46
47
  Tuple of (existing_record_ids, existing_pks_map)
@@ -50,6 +51,9 @@ class RecordClassifier:
50
51
  if not unique_fields or not objs:
51
52
  return set(), {}
52
53
 
54
+ # Use query_model if provided (for MTI scenarios), otherwise use self.model_cls
55
+ query_model = query_model or self.model_cls
56
+
53
57
  # Build a query to find existing records
54
58
  queries = []
55
59
  obj_to_unique_values = {}
@@ -77,10 +81,10 @@ class RecordClassifier:
77
81
  for q in queries[1:]:
78
82
  combined_query |= q
79
83
 
80
- logger.info(f"Classifying for upsert: model={self.model_cls.__name__}, query={combined_query}, unique_fields={unique_fields}")
81
- queryset = self.model_cls.objects.filter(combined_query)
84
+ logger.info(f"Classifying for upsert: model={query_model.__name__}, query={combined_query}, unique_fields={unique_fields}")
85
+ queryset = query_model.objects.filter(combined_query)
82
86
  logger.info(f"Queryset SQL: {queryset.query}")
83
- logger.info(f"All records in table: {self.model_cls.objects.all().count()}")
87
+ logger.info(f"All records in table: {query_model.objects.all().count()}")
84
88
  existing_records = list(queryset.values("pk", *unique_fields))
85
89
  logger.info(f"Found {len(existing_records)} existing records: {existing_records}")
86
90
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "django-bulk-hooks"
3
- version = "0.2.46"
3
+ version = "0.2.47"
4
4
  description = "Hook-style hooks for Django bulk operations like bulk_create and bulk_update."
5
5
  authors = ["Konrad Beck <konrad.beck@merchantcapital.co.za>"]
6
6
  readme = "README.md"