django-bulk-hooks 0.2.47__tar.gz → 0.2.50__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.47 → django_bulk_hooks-0.2.50}/PKG-INFO +1 -1
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/operations/bulk_executor.py +46 -17
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/operations/mti_handler.py +21 -20
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/pyproject.toml +1 -1
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/LICENSE +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/README.md +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/changeset.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/dispatcher.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/factory.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/helpers.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/operations/__init__.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/operations/analyzer.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/operations/coordinator.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/operations/mti_plans.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/operations/record_classifier.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/registry.py +0 -0
{django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/operations/bulk_executor.py
RENAMED
|
@@ -107,7 +107,7 @@ class BulkExecutor:
|
|
|
107
107
|
|
|
108
108
|
# Tag objects with upsert metadata for hook dispatching
|
|
109
109
|
if update_conflicts and unique_fields:
|
|
110
|
-
self._tag_upsert_metadata(result, existing_record_ids)
|
|
110
|
+
self._tag_upsert_metadata(result, existing_record_ids, existing_pks_map)
|
|
111
111
|
|
|
112
112
|
return result
|
|
113
113
|
|
|
@@ -126,8 +126,8 @@ class BulkExecutor:
|
|
|
126
126
|
if update_conflicts and unique_fields:
|
|
127
127
|
# Use pre-classified results if available, otherwise classify now
|
|
128
128
|
if existing_record_ids is None:
|
|
129
|
-
existing_record_ids,
|
|
130
|
-
self._tag_upsert_metadata(result, existing_record_ids)
|
|
129
|
+
existing_record_ids, existing_pks_map = self.record_classifier.classify_for_upsert(objs, unique_fields)
|
|
130
|
+
self._tag_upsert_metadata(result, existing_record_ids, existing_pks_map)
|
|
131
131
|
|
|
132
132
|
return result
|
|
133
133
|
|
|
@@ -243,9 +243,11 @@ class BulkExecutor:
|
|
|
243
243
|
# Copy generated fields back to parent objects
|
|
244
244
|
for upserted_parent, parent_obj in zip(upserted_parents, parent_level.objects):
|
|
245
245
|
for field in parent_level.model_class._meta.local_fields:
|
|
246
|
-
|
|
246
|
+
# Use attname for ForeignKey fields to avoid triggering database queries
|
|
247
|
+
field_attr = field.attname if isinstance(field, ForeignKey) else field.name
|
|
248
|
+
upserted_value = getattr(upserted_parent, field_attr, None)
|
|
247
249
|
if upserted_value is not None:
|
|
248
|
-
setattr(parent_obj,
|
|
250
|
+
setattr(parent_obj, field_attr, upserted_value)
|
|
249
251
|
|
|
250
252
|
parent_obj._state.adding = False
|
|
251
253
|
parent_obj._state.db = self.queryset.db
|
|
@@ -291,14 +293,36 @@ class BulkExecutor:
|
|
|
291
293
|
# In MTI, child objects get PKs from parent links, but we need to distinguish
|
|
292
294
|
# between truly new records and existing records for upsert operations
|
|
293
295
|
objs_without_pk, objs_with_pk = [], []
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
296
|
+
|
|
297
|
+
# Check which CHILD records actually exist in the child table
|
|
298
|
+
# This is separate from checking parent existence
|
|
299
|
+
if plan.update_conflicts:
|
|
300
|
+
# Query the CHILD table to see which child records exist
|
|
301
|
+
parent_pks = []
|
|
302
|
+
for child_obj in plan.child_objects:
|
|
303
|
+
child_pk = getattr(child_obj, plan.child_model._meta.pk.attname, None)
|
|
304
|
+
if child_pk:
|
|
305
|
+
parent_pks.append(child_pk)
|
|
306
|
+
|
|
307
|
+
existing_child_pks = set()
|
|
308
|
+
if parent_pks:
|
|
309
|
+
existing_child_pks = set(
|
|
310
|
+
base_qs.filter(pk__in=parent_pks).values_list('pk', flat=True)
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Split based on whether child record exists
|
|
314
|
+
for child_obj in plan.child_objects:
|
|
315
|
+
child_pk = getattr(child_obj, plan.child_model._meta.pk.attname, None)
|
|
316
|
+
if child_pk and child_pk in existing_child_pks:
|
|
317
|
+
# Child record exists - update it
|
|
318
|
+
objs_with_pk.append(child_obj)
|
|
319
|
+
else:
|
|
320
|
+
# Child record doesn't exist - insert it
|
|
321
|
+
objs_without_pk.append(child_obj)
|
|
322
|
+
else:
|
|
323
|
+
# Not an upsert - all are new records
|
|
324
|
+
objs_without_pk = plan.child_objects
|
|
325
|
+
objs_with_pk = []
|
|
302
326
|
|
|
303
327
|
# For objects with PK (existing records in upsert), use bulk_update
|
|
304
328
|
if objs_with_pk and plan.update_fields:
|
|
@@ -507,7 +531,7 @@ class BulkExecutor:
|
|
|
507
531
|
|
|
508
532
|
return QuerySet.delete(self.queryset)
|
|
509
533
|
|
|
510
|
-
def _tag_upsert_metadata(self, result_objects, existing_record_ids):
|
|
534
|
+
def _tag_upsert_metadata(self, result_objects, existing_record_ids, existing_pks_map):
|
|
511
535
|
"""
|
|
512
536
|
Tag objects with metadata indicating whether they were created or updated.
|
|
513
537
|
|
|
@@ -517,13 +541,18 @@ class BulkExecutor:
|
|
|
517
541
|
Args:
|
|
518
542
|
result_objects: List of objects returned from bulk operation
|
|
519
543
|
existing_record_ids: Set of id() for objects that existed before the operation
|
|
544
|
+
existing_pks_map: Dict mapping id(obj) -> pk for existing records
|
|
520
545
|
"""
|
|
521
546
|
created_count = 0
|
|
522
547
|
updated_count = 0
|
|
523
548
|
|
|
549
|
+
# Create a set of PKs that existed before the operation
|
|
550
|
+
existing_pks = set(existing_pks_map.values())
|
|
551
|
+
|
|
524
552
|
for obj in result_objects:
|
|
525
|
-
#
|
|
526
|
-
|
|
553
|
+
# Use PK to determine if this record was created or updated
|
|
554
|
+
# If the PK was in the existing_pks_map, it was updated; otherwise created
|
|
555
|
+
was_created = obj.pk not in existing_pks
|
|
527
556
|
obj._bulk_hooks_was_created = was_created
|
|
528
557
|
obj._bulk_hooks_upsert_metadata = True
|
|
529
558
|
|
|
@@ -534,6 +563,6 @@ class BulkExecutor:
|
|
|
534
563
|
|
|
535
564
|
logger.info(
|
|
536
565
|
f"Tagged upsert metadata: {created_count} created, {updated_count} updated "
|
|
537
|
-
f"(total={len(result_objects)},
|
|
566
|
+
f"(total={len(result_objects)}, existing_pks={len(existing_pks)})"
|
|
538
567
|
)
|
|
539
568
|
|
{django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/operations/mti_handler.py
RENAMED
|
@@ -78,8 +78,7 @@ class MTIHandler:
|
|
|
78
78
|
chain.append(current_model)
|
|
79
79
|
|
|
80
80
|
# Get concrete parent models (not abstract, not proxy)
|
|
81
|
-
parents = [parent for parent in current_model._meta.parents.keys()
|
|
82
|
-
if not parent._meta.proxy and not parent._meta.abstract]
|
|
81
|
+
parents = [parent for parent in current_model._meta.parents.keys() if not parent._meta.proxy and not parent._meta.abstract]
|
|
83
82
|
|
|
84
83
|
current_model = parents[0] if parents else None
|
|
85
84
|
|
|
@@ -113,31 +112,33 @@ class MTIHandler:
|
|
|
113
112
|
|
|
114
113
|
def find_model_with_unique_fields(self, unique_fields):
|
|
115
114
|
"""
|
|
116
|
-
Find which model in the inheritance chain
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
Find which model in the inheritance chain to query for existing records.
|
|
116
|
+
|
|
117
|
+
For MTI upsert operations, we need to determine if the parent record exists
|
|
118
|
+
to properly fire AFTER_CREATE vs AFTER_UPDATE hooks. This is critical because:
|
|
119
|
+
- If parent exists but child doesn't: creating child for existing parent → AFTER_UPDATE
|
|
120
|
+
- If neither exists: creating both parent and child → AFTER_CREATE
|
|
121
|
+
|
|
122
|
+
Therefore, we return the root parent model to check if the parent record exists,
|
|
123
|
+
regardless of where the unique fields are defined.
|
|
124
|
+
|
|
121
125
|
Args:
|
|
122
126
|
unique_fields: List of field names forming the unique constraint
|
|
123
|
-
|
|
127
|
+
|
|
124
128
|
Returns:
|
|
125
|
-
Model class
|
|
129
|
+
Model class to query for existing records (root parent for MTI)
|
|
126
130
|
"""
|
|
127
131
|
if not unique_fields:
|
|
128
132
|
return self.model_cls
|
|
129
|
-
|
|
133
|
+
|
|
130
134
|
inheritance_chain = self.get_inheritance_chain()
|
|
131
|
-
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return model_cls
|
|
139
|
-
|
|
140
|
-
# Fallback to child model (shouldn't happen if unique_fields are valid)
|
|
135
|
+
|
|
136
|
+
# For MTI models with multiple levels, return the root parent model
|
|
137
|
+
# This ensures we check if the parent exists, which determines create vs update hooks
|
|
138
|
+
if len(inheritance_chain) > 1:
|
|
139
|
+
return inheritance_chain[0] # Root parent model
|
|
140
|
+
|
|
141
|
+
# For non-MTI models (shouldn't happen, but safe fallback)
|
|
141
142
|
return self.model_cls
|
|
142
143
|
|
|
143
144
|
# ==================== MTI BULK CREATE PLANNING ====================
|
|
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.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/operations/__init__.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/operations/analyzer.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/operations/coordinator.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.47 → django_bulk_hooks-0.2.50}/django_bulk_hooks/operations/mti_plans.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|