django-bulk-hooks 0.2.46__tar.gz → 0.2.48__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.46 → django_bulk_hooks-0.2.48}/PKG-INFO +1 -1
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/operations/bulk_executor.py +20 -8
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/operations/coordinator.py +9 -1
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/operations/mti_handler.py +29 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/operations/record_classifier.py +8 -4
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/pyproject.toml +1 -1
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/LICENSE +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/README.md +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/changeset.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/dispatcher.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/factory.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/helpers.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/operations/__init__.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/operations/analyzer.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/operations/mti_plans.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/registry.py +0 -0
{django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/operations/bulk_executor.py
RENAMED
|
@@ -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
|
-
|
|
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}")
|
|
@@ -100,7 +107,7 @@ class BulkExecutor:
|
|
|
100
107
|
|
|
101
108
|
# Tag objects with upsert metadata for hook dispatching
|
|
102
109
|
if update_conflicts and unique_fields:
|
|
103
|
-
self._tag_upsert_metadata(result, existing_record_ids)
|
|
110
|
+
self._tag_upsert_metadata(result, existing_record_ids, existing_pks_map)
|
|
104
111
|
|
|
105
112
|
return result
|
|
106
113
|
|
|
@@ -119,8 +126,8 @@ class BulkExecutor:
|
|
|
119
126
|
if update_conflicts and unique_fields:
|
|
120
127
|
# Use pre-classified results if available, otherwise classify now
|
|
121
128
|
if existing_record_ids is None:
|
|
122
|
-
existing_record_ids,
|
|
123
|
-
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)
|
|
124
131
|
|
|
125
132
|
return result
|
|
126
133
|
|
|
@@ -500,7 +507,7 @@ class BulkExecutor:
|
|
|
500
507
|
|
|
501
508
|
return QuerySet.delete(self.queryset)
|
|
502
509
|
|
|
503
|
-
def _tag_upsert_metadata(self, result_objects, existing_record_ids):
|
|
510
|
+
def _tag_upsert_metadata(self, result_objects, existing_record_ids, existing_pks_map):
|
|
504
511
|
"""
|
|
505
512
|
Tag objects with metadata indicating whether they were created or updated.
|
|
506
513
|
|
|
@@ -510,13 +517,18 @@ class BulkExecutor:
|
|
|
510
517
|
Args:
|
|
511
518
|
result_objects: List of objects returned from bulk operation
|
|
512
519
|
existing_record_ids: Set of id() for objects that existed before the operation
|
|
520
|
+
existing_pks_map: Dict mapping id(obj) -> pk for existing records
|
|
513
521
|
"""
|
|
514
522
|
created_count = 0
|
|
515
523
|
updated_count = 0
|
|
516
524
|
|
|
525
|
+
# Create a set of PKs that existed before the operation
|
|
526
|
+
existing_pks = set(existing_pks_map.values())
|
|
527
|
+
|
|
517
528
|
for obj in result_objects:
|
|
518
|
-
#
|
|
519
|
-
|
|
529
|
+
# Use PK to determine if this record was created or updated
|
|
530
|
+
# If the PK was in the existing_pks_map, it was updated; otherwise created
|
|
531
|
+
was_created = obj.pk not in existing_pks
|
|
520
532
|
obj._bulk_hooks_was_created = was_created
|
|
521
533
|
obj._bulk_hooks_upsert_metadata = True
|
|
522
534
|
|
|
@@ -527,6 +539,6 @@ class BulkExecutor:
|
|
|
527
539
|
|
|
528
540
|
logger.info(
|
|
529
541
|
f"Tagged upsert metadata: {created_count} created, {updated_count} updated "
|
|
530
|
-
f"(total={len(result_objects)},
|
|
542
|
+
f"(total={len(result_objects)}, existing_pks={len(existing_pks)})"
|
|
531
543
|
)
|
|
532
544
|
|
{django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/operations/coordinator.py
RENAMED
|
@@ -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
|
-
|
|
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}")
|
{django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/operations/mti_handler.py
RENAMED
|
@@ -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={
|
|
81
|
-
queryset =
|
|
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: {
|
|
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
|
|
|
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.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/operations/__init__.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/operations/analyzer.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.46 → django_bulk_hooks-0.2.48}/django_bulk_hooks/operations/mti_plans.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|