django-bulk-hooks 0.2.61__py3-none-any.whl → 0.2.62__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/changeset.py +214 -211
- django_bulk_hooks/conditions.py +7 -3
- django_bulk_hooks/decorators.py +5 -15
- django_bulk_hooks/dispatcher.py +2 -6
- django_bulk_hooks/handler.py +2 -2
- django_bulk_hooks/helpers.py +251 -245
- django_bulk_hooks/manager.py +150 -150
- django_bulk_hooks/models.py +74 -87
- django_bulk_hooks/operations/analyzer.py +319 -319
- django_bulk_hooks/operations/bulk_executor.py +22 -31
- django_bulk_hooks/operations/coordinator.py +10 -7
- django_bulk_hooks/operations/field_utils.py +5 -13
- django_bulk_hooks/operations/mti_handler.py +10 -5
- django_bulk_hooks/operations/mti_plans.py +103 -103
- django_bulk_hooks/operations/record_classifier.py +1 -1
- django_bulk_hooks/queryset.py +5 -1
- django_bulk_hooks/registry.py +0 -2
- {django_bulk_hooks-0.2.61.dist-info → django_bulk_hooks-0.2.62.dist-info}/METADATA +1 -1
- django_bulk_hooks-0.2.62.dist-info/RECORD +27 -0
- django_bulk_hooks-0.2.61.dist-info/RECORD +0 -27
- {django_bulk_hooks-0.2.61.dist-info → django_bulk_hooks-0.2.62.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.2.61.dist-info → django_bulk_hooks-0.2.62.dist-info}/WHEEL +0 -0
|
@@ -47,8 +47,9 @@ class BulkExecutor:
|
|
|
47
47
|
self.record_classifier = record_classifier
|
|
48
48
|
self.model_cls = queryset.model
|
|
49
49
|
|
|
50
|
-
def _handle_upsert_metadata_tagging(
|
|
51
|
-
|
|
50
|
+
def _handle_upsert_metadata_tagging(
|
|
51
|
+
self, result_objects, objs, update_conflicts, unique_fields, existing_record_ids=None, existing_pks_map=None
|
|
52
|
+
):
|
|
52
53
|
"""
|
|
53
54
|
|
|
54
55
|
Handle classification and metadata tagging for upsert operations.
|
|
@@ -82,19 +83,13 @@ class BulkExecutor:
|
|
|
82
83
|
"""
|
|
83
84
|
|
|
84
85
|
if not (update_conflicts and unique_fields):
|
|
85
|
-
|
|
86
86
|
return
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
88
|
# Classify records if not already done
|
|
91
89
|
|
|
92
90
|
if existing_record_ids is None or existing_pks_map is None:
|
|
93
|
-
|
|
94
91
|
existing_record_ids, existing_pks_map = self.record_classifier.classify_for_upsert(objs, unique_fields)
|
|
95
92
|
|
|
96
|
-
|
|
97
|
-
|
|
98
93
|
# Tag the metadata
|
|
99
94
|
tag_upsert_metadata(result_objects, existing_record_ids, existing_pks_map)
|
|
100
95
|
|
|
@@ -142,11 +137,13 @@ class BulkExecutor:
|
|
|
142
137
|
# This handles the schema migration case where parent exists but child doesn't
|
|
143
138
|
query_model = self.mti_handler.find_model_with_unique_fields(unique_fields)
|
|
144
139
|
logger.info(f"MTI upsert: querying {query_model.__name__} for unique fields {unique_fields}")
|
|
145
|
-
|
|
140
|
+
|
|
146
141
|
existing_record_ids, existing_pks_map = self.record_classifier.classify_for_upsert(
|
|
147
142
|
objs, unique_fields, query_model=query_model
|
|
148
143
|
)
|
|
149
|
-
logger.info(
|
|
144
|
+
logger.info(
|
|
145
|
+
f"MTI Upsert classification: {len(existing_record_ids)} existing, {len(objs) - len(existing_record_ids)} new"
|
|
146
|
+
)
|
|
150
147
|
logger.info(f"existing_record_ids: {existing_record_ids}")
|
|
151
148
|
logger.info(f"existing_pks_map: {existing_pks_map}")
|
|
152
149
|
|
|
@@ -176,10 +173,7 @@ class BulkExecutor:
|
|
|
176
173
|
)
|
|
177
174
|
|
|
178
175
|
# Unified upsert metadata handling for both paths
|
|
179
|
-
self._handle_upsert_metadata_tagging(
|
|
180
|
-
result, objs, update_conflicts, unique_fields,
|
|
181
|
-
existing_record_ids, existing_pks_map
|
|
182
|
-
)
|
|
176
|
+
self._handle_upsert_metadata_tagging(result, objs, update_conflicts, unique_fields, existing_record_ids, existing_pks_map)
|
|
183
177
|
|
|
184
178
|
return result
|
|
185
179
|
|
|
@@ -243,9 +237,8 @@ class BulkExecutor:
|
|
|
243
237
|
|
|
244
238
|
# Use unified auto-now field handling
|
|
245
239
|
from django_bulk_hooks.operations.field_utils import handle_auto_now_fields_for_inheritance_chain
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
)
|
|
240
|
+
|
|
241
|
+
auto_now_fields = handle_auto_now_fields_for_inheritance_chain(models_to_check, objs, for_update=True)
|
|
249
242
|
|
|
250
243
|
# Add auto_now fields to the update list if not already present
|
|
251
244
|
for auto_now_field in auto_now_fields:
|
|
@@ -287,7 +280,6 @@ class BulkExecutor:
|
|
|
287
280
|
if not plan:
|
|
288
281
|
return []
|
|
289
282
|
|
|
290
|
-
|
|
291
283
|
with transaction.atomic(using=self.queryset.db, savepoint=False):
|
|
292
284
|
# Step 1: Upsert all parent objects level by level using Django's native upsert
|
|
293
285
|
parent_instances_map = {} # Maps original obj id() -> {model: parent_instance}
|
|
@@ -295,15 +287,15 @@ class BulkExecutor:
|
|
|
295
287
|
for parent_level in plan.parent_levels:
|
|
296
288
|
# Use base QuerySet to avoid recursion
|
|
297
289
|
base_qs = BaseQuerySet(model=parent_level.model_class, using=self.queryset.db)
|
|
298
|
-
|
|
290
|
+
|
|
299
291
|
# Build bulk_create kwargs
|
|
300
292
|
bulk_kwargs = {"batch_size": len(parent_level.objects)}
|
|
301
|
-
|
|
293
|
+
|
|
302
294
|
if parent_level.update_conflicts:
|
|
303
295
|
# Let Django handle the upsert - it will INSERT or UPDATE as needed
|
|
304
296
|
bulk_kwargs["update_conflicts"] = True
|
|
305
297
|
bulk_kwargs["unique_fields"] = parent_level.unique_fields
|
|
306
|
-
|
|
298
|
+
|
|
307
299
|
# Filter update fields to only those that exist in this parent model
|
|
308
300
|
parent_model_fields = {field.name for field in parent_level.model_class._meta.local_fields}
|
|
309
301
|
filtered_update_fields = [field for field in parent_level.update_fields if field in parent_model_fields]
|
|
@@ -368,9 +360,7 @@ class BulkExecutor:
|
|
|
368
360
|
|
|
369
361
|
existing_child_pks = set()
|
|
370
362
|
if parent_pks_to_check:
|
|
371
|
-
existing_child_pks = set(
|
|
372
|
-
base_qs.filter(pk__in=parent_pks_to_check).values_list('pk', flat=True)
|
|
373
|
-
)
|
|
363
|
+
existing_child_pks = set(base_qs.filter(pk__in=parent_pks_to_check).values_list("pk", flat=True))
|
|
374
364
|
|
|
375
365
|
# Split based on whether child record exists
|
|
376
366
|
for child_obj in plan.child_objects:
|
|
@@ -430,15 +420,17 @@ class BulkExecutor:
|
|
|
430
420
|
|
|
431
421
|
# Build kwargs for _batched_insert call
|
|
432
422
|
kwargs = {
|
|
433
|
-
|
|
423
|
+
"batch_size": len(objs_without_pk),
|
|
434
424
|
}
|
|
435
425
|
# Only pass conflict resolution parameters if we have unique fields for this table
|
|
436
426
|
if batched_unique_fields:
|
|
437
|
-
kwargs.update(
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
427
|
+
kwargs.update(
|
|
428
|
+
{
|
|
429
|
+
"on_conflict": on_conflict,
|
|
430
|
+
"update_fields": batched_update_fields,
|
|
431
|
+
"unique_fields": batched_unique_fields,
|
|
432
|
+
}
|
|
433
|
+
)
|
|
442
434
|
|
|
443
435
|
returned_columns = base_qs._batched_insert(
|
|
444
436
|
objs_without_pk,
|
|
@@ -623,4 +615,3 @@ class BulkExecutor:
|
|
|
623
615
|
from django.db.models import QuerySet
|
|
624
616
|
|
|
625
617
|
return QuerySet.delete(self.queryset)
|
|
626
|
-
|
|
@@ -68,24 +68,28 @@ class BulkOperationCoordinator:
|
|
|
68
68
|
def analyzer(self):
|
|
69
69
|
"""Get or create ModelAnalyzer"""
|
|
70
70
|
from django_bulk_hooks.operations.analyzer import ModelAnalyzer
|
|
71
|
+
|
|
71
72
|
return self._get_or_create_service("analyzer", ModelAnalyzer, self.model_cls)
|
|
72
73
|
|
|
73
74
|
@property
|
|
74
75
|
def mti_handler(self):
|
|
75
76
|
"""Get or create MTIHandler"""
|
|
76
77
|
from django_bulk_hooks.operations.mti_handler import MTIHandler
|
|
78
|
+
|
|
77
79
|
return self._get_or_create_service("mti_handler", MTIHandler, self.model_cls)
|
|
78
80
|
|
|
79
81
|
@property
|
|
80
82
|
def record_classifier(self):
|
|
81
83
|
"""Get or create RecordClassifier"""
|
|
82
84
|
from django_bulk_hooks.operations.record_classifier import RecordClassifier
|
|
85
|
+
|
|
83
86
|
return self._get_or_create_service("record_classifier", RecordClassifier, self.model_cls)
|
|
84
87
|
|
|
85
88
|
@property
|
|
86
89
|
def executor(self):
|
|
87
90
|
"""Get or create BulkExecutor"""
|
|
88
91
|
from django_bulk_hooks.operations.bulk_executor import BulkExecutor
|
|
92
|
+
|
|
89
93
|
return self._get_or_create_service(
|
|
90
94
|
"executor",
|
|
91
95
|
BulkExecutor,
|
|
@@ -99,6 +103,7 @@ class BulkOperationCoordinator:
|
|
|
99
103
|
def dispatcher(self):
|
|
100
104
|
"""Get or create Dispatcher"""
|
|
101
105
|
from django_bulk_hooks.dispatcher import get_dispatcher
|
|
106
|
+
|
|
102
107
|
return self._get_or_create_service("dispatcher", get_dispatcher)
|
|
103
108
|
|
|
104
109
|
@property
|
|
@@ -171,7 +176,7 @@ class BulkOperationCoordinator:
|
|
|
171
176
|
Returns:
|
|
172
177
|
List of created objects
|
|
173
178
|
"""
|
|
174
|
-
empty_result = self._validate_objects_for_operation(objs,
|
|
179
|
+
empty_result = self._validate_objects_for_operation(objs, "create")
|
|
175
180
|
if empty_result is not None:
|
|
176
181
|
return empty_result
|
|
177
182
|
|
|
@@ -188,9 +193,7 @@ class BulkOperationCoordinator:
|
|
|
188
193
|
query_model = self.mti_handler.find_model_with_unique_fields(unique_fields)
|
|
189
194
|
logger.info(f"MTI model detected: querying {query_model.__name__} for unique fields {unique_fields}")
|
|
190
195
|
|
|
191
|
-
existing_record_ids, existing_pks_map = self.record_classifier.classify_for_upsert(
|
|
192
|
-
objs, unique_fields, query_model=query_model
|
|
193
|
-
)
|
|
196
|
+
existing_record_ids, existing_pks_map = self.record_classifier.classify_for_upsert(objs, unique_fields, query_model=query_model)
|
|
194
197
|
logger.info(f"Upsert operation: {len(existing_record_ids)} existing, {len(objs) - len(existing_record_ids)} new records")
|
|
195
198
|
logger.debug(f"Existing record IDs: {existing_record_ids}")
|
|
196
199
|
logger.debug(f"Existing PKs map: {existing_pks_map}")
|
|
@@ -249,7 +252,7 @@ class BulkOperationCoordinator:
|
|
|
249
252
|
Returns:
|
|
250
253
|
Number of objects updated
|
|
251
254
|
"""
|
|
252
|
-
empty_result = self._validate_objects_for_operation(objs,
|
|
255
|
+
empty_result = self._validate_objects_for_operation(objs, "update")
|
|
253
256
|
if empty_result is not None:
|
|
254
257
|
return empty_result
|
|
255
258
|
|
|
@@ -544,7 +547,7 @@ class BulkOperationCoordinator:
|
|
|
544
547
|
"""
|
|
545
548
|
# Get objects
|
|
546
549
|
objs = list(self.queryset)
|
|
547
|
-
empty_result = self._validate_objects_for_operation(objs,
|
|
550
|
+
empty_result = self._validate_objects_for_operation(objs, "delete")
|
|
548
551
|
if empty_result is not None:
|
|
549
552
|
return empty_result
|
|
550
553
|
|
|
@@ -581,7 +584,7 @@ class BulkOperationCoordinator:
|
|
|
581
584
|
Returns:
|
|
582
585
|
None
|
|
583
586
|
"""
|
|
584
|
-
empty_result = self._validate_objects_for_operation(objs,
|
|
587
|
+
empty_result = self._validate_objects_for_operation(objs, "validate")
|
|
585
588
|
if empty_result is not None:
|
|
586
589
|
return
|
|
587
590
|
|
|
@@ -65,10 +65,7 @@ def get_field_values_for_db(obj, field_names, model_cls=None):
|
|
|
65
65
|
if model_cls is None:
|
|
66
66
|
model_cls = obj.__class__
|
|
67
67
|
|
|
68
|
-
return {
|
|
69
|
-
field_name: get_field_value_for_db(obj, field_name, model_cls)
|
|
70
|
-
for field_name in field_names
|
|
71
|
-
}
|
|
68
|
+
return {field_name: get_field_value_for_db(obj, field_name, model_cls) for field_name in field_names}
|
|
72
69
|
|
|
73
70
|
|
|
74
71
|
def normalize_field_name_to_db(field_name, model_cls):
|
|
@@ -152,9 +149,7 @@ def get_auto_fields(model_cls, include_auto_now_add=True):
|
|
|
152
149
|
"""
|
|
153
150
|
fields = []
|
|
154
151
|
for field in model_cls._meta.fields:
|
|
155
|
-
if getattr(field, "auto_now", False) or (
|
|
156
|
-
include_auto_now_add and getattr(field, "auto_now_add", False)
|
|
157
|
-
):
|
|
152
|
+
if getattr(field, "auto_now", False) or (include_auto_now_add and getattr(field, "auto_now_add", False)):
|
|
158
153
|
fields.append(field.name)
|
|
159
154
|
return fields
|
|
160
155
|
|
|
@@ -166,8 +161,7 @@ def get_auto_now_only_fields(model_cls):
|
|
|
166
161
|
|
|
167
162
|
def get_fk_fields(model_cls):
|
|
168
163
|
"""Get foreign key field names for a model."""
|
|
169
|
-
return [field.name for field in model_cls._meta.concrete_fields
|
|
170
|
-
if field.is_relation and not field.many_to_many]
|
|
164
|
+
return [field.name for field in model_cls._meta.concrete_fields if field.is_relation and not field.many_to_many]
|
|
171
165
|
|
|
172
166
|
|
|
173
167
|
def collect_auto_now_fields_for_inheritance_chain(inheritance_chain):
|
|
@@ -199,9 +193,7 @@ def handle_auto_now_fields_for_inheritance_chain(models, instances, for_update=T
|
|
|
199
193
|
for field in model_cls._meta.local_fields:
|
|
200
194
|
# For updates, only include auto_now (not auto_now_add)
|
|
201
195
|
# For creates, include both
|
|
202
|
-
if getattr(field, "auto_now", False) or (
|
|
203
|
-
not for_update and getattr(field, "auto_now_add", False)
|
|
204
|
-
):
|
|
196
|
+
if getattr(field, "auto_now", False) or (not for_update and getattr(field, "auto_now_add", False)):
|
|
205
197
|
all_auto_now_fields.add(field.name)
|
|
206
198
|
|
|
207
199
|
# Pre-save the field on instances
|
|
@@ -231,4 +223,4 @@ def pre_save_auto_now_fields(objects, inheritance_chain):
|
|
|
231
223
|
field.pre_save(obj, add=False)
|
|
232
224
|
break
|
|
233
225
|
except Exception:
|
|
234
|
-
continue
|
|
226
|
+
continue
|
|
@@ -219,6 +219,7 @@ class MTIHandler:
|
|
|
219
219
|
|
|
220
220
|
# Pre-compute child-specific fields for execution efficiency
|
|
221
221
|
from django_bulk_hooks.helpers import get_fields_for_model, filter_field_names_for_model
|
|
222
|
+
|
|
222
223
|
child_unique_fields = get_fields_for_model(inheritance_chain[-1], unique_fields or [])
|
|
223
224
|
child_update_fields = get_fields_for_model(inheritance_chain[-1], update_fields or [])
|
|
224
225
|
|
|
@@ -373,14 +374,14 @@ class MTIHandler:
|
|
|
373
374
|
def _get_auto_now_fields_for_model(self, model_class, model_fields_by_name):
|
|
374
375
|
"""
|
|
375
376
|
Get auto_now (not auto_now_add) fields for a specific model.
|
|
376
|
-
|
|
377
|
+
|
|
377
378
|
Only includes fields that exist in model_fields_by_name to ensure
|
|
378
379
|
they're valid local fields for this model level.
|
|
379
|
-
|
|
380
|
+
|
|
380
381
|
Args:
|
|
381
382
|
model_class: Model class to get fields for
|
|
382
383
|
model_fields_by_name: Dict of valid field names for this model level
|
|
383
|
-
|
|
384
|
+
|
|
384
385
|
Returns:
|
|
385
386
|
List of auto_now field names (excluding auto_now_add)
|
|
386
387
|
"""
|
|
@@ -470,7 +471,9 @@ class MTIHandler:
|
|
|
470
471
|
|
|
471
472
|
# Handle auto fields for this single parent model
|
|
472
473
|
handle_auto_now_fields_for_inheritance_chain(
|
|
473
|
-
[parent_model],
|
|
474
|
+
[parent_model],
|
|
475
|
+
[parent_obj],
|
|
476
|
+
for_update=False, # MTI create is like insert
|
|
474
477
|
)
|
|
475
478
|
|
|
476
479
|
return parent_obj
|
|
@@ -527,7 +530,9 @@ class MTIHandler:
|
|
|
527
530
|
|
|
528
531
|
# Handle auto fields for this single child model
|
|
529
532
|
handle_auto_now_fields_for_inheritance_chain(
|
|
530
|
-
[child_model],
|
|
533
|
+
[child_model],
|
|
534
|
+
[child_obj],
|
|
535
|
+
for_update=False, # MTI create is like insert
|
|
531
536
|
)
|
|
532
537
|
|
|
533
538
|
return child_obj
|
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
"""
|
|
2
|
-
MTI operation plans - Data structures for multi-table inheritance operations.
|
|
3
|
-
|
|
4
|
-
These are pure data structures returned by MTIHandler to be executed by BulkExecutor.
|
|
5
|
-
This separates planning (logic) from execution (database operations).
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from dataclasses import dataclass
|
|
9
|
-
from dataclasses import field
|
|
10
|
-
from typing import Any
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@dataclass
|
|
14
|
-
class ParentLevel:
|
|
15
|
-
"""
|
|
16
|
-
Represents one level in the parent hierarchy for MTI bulk create.
|
|
17
|
-
|
|
18
|
-
Attributes:
|
|
19
|
-
model_class: The parent model class for this level
|
|
20
|
-
objects: List of parent instances to create
|
|
21
|
-
original_object_map: Maps parent instance id() -> original object id()
|
|
22
|
-
update_conflicts: Whether to enable UPSERT for this level
|
|
23
|
-
unique_fields: Fields for conflict detection (if update_conflicts=True)
|
|
24
|
-
update_fields: Fields to update on conflict (if update_conflicts=True)
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
model_class: Any
|
|
28
|
-
objects: list[Any]
|
|
29
|
-
original_object_map: dict[int, int] = field(default_factory=dict)
|
|
30
|
-
update_conflicts: bool = False
|
|
31
|
-
unique_fields: list[str] = field(default_factory=list)
|
|
32
|
-
update_fields: list[str] = field(default_factory=list)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@dataclass
|
|
36
|
-
class MTICreatePlan:
|
|
37
|
-
"""
|
|
38
|
-
Plan for executing bulk_create on an MTI model.
|
|
39
|
-
|
|
40
|
-
This plan describes WHAT to create, not HOW to create it.
|
|
41
|
-
The executor is responsible for executing this plan.
|
|
42
|
-
|
|
43
|
-
Attributes:
|
|
44
|
-
inheritance_chain: List of model classes from root to child
|
|
45
|
-
parent_levels: List of ParentLevel objects, one per parent model
|
|
46
|
-
child_objects: List of child instances to create (not yet with parent links)
|
|
47
|
-
child_model: The child model class
|
|
48
|
-
original_objects: Original objects provided by user
|
|
49
|
-
batch_size: Batch size for operations
|
|
50
|
-
existing_record_ids: Set of id() of original objects that represent existing DB records
|
|
51
|
-
update_conflicts: Whether this is an upsert operation
|
|
52
|
-
unique_fields: Fields used for conflict detection (original, unfiltered)
|
|
53
|
-
update_fields: Fields to update on conflict (original, unfiltered)
|
|
54
|
-
child_unique_fields: Pre-filtered field objects for child table conflict detection
|
|
55
|
-
child_update_fields: Pre-filtered field objects for child table updates
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
inheritance_chain: list[Any]
|
|
59
|
-
parent_levels: list[ParentLevel]
|
|
60
|
-
child_objects: list[Any]
|
|
61
|
-
child_model: Any
|
|
62
|
-
original_objects: list[Any]
|
|
63
|
-
batch_size: int = None
|
|
64
|
-
existing_record_ids: set = field(default_factory=set)
|
|
65
|
-
update_conflicts: bool = False
|
|
66
|
-
unique_fields: list[str] = field(default_factory=list)
|
|
67
|
-
update_fields: list[str] = field(default_factory=list)
|
|
68
|
-
child_unique_fields: list = field(default_factory=list) # Field objects for child table
|
|
69
|
-
child_update_fields: list = field(default_factory=list) # Field objects for child table
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@dataclass
|
|
73
|
-
class ModelFieldGroup:
|
|
74
|
-
"""
|
|
75
|
-
Represents fields to update for one model in the inheritance chain.
|
|
76
|
-
|
|
77
|
-
Attributes:
|
|
78
|
-
model_class: The model class
|
|
79
|
-
fields: List of field names to update on this model
|
|
80
|
-
filter_field: Field to use for filtering (e.g., 'pk' or parent link attname)
|
|
81
|
-
"""
|
|
82
|
-
|
|
83
|
-
model_class: Any
|
|
84
|
-
fields: list[str]
|
|
85
|
-
filter_field: str = "pk"
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
@dataclass
|
|
89
|
-
class MTIUpdatePlan:
|
|
90
|
-
"""
|
|
91
|
-
Plan for executing bulk_update on an MTI model.
|
|
92
|
-
|
|
93
|
-
Attributes:
|
|
94
|
-
inheritance_chain: List of model classes from root to child
|
|
95
|
-
field_groups: List of ModelFieldGroup objects
|
|
96
|
-
objects: Objects to update
|
|
97
|
-
batch_size: Batch size for operations
|
|
98
|
-
"""
|
|
99
|
-
|
|
100
|
-
inheritance_chain: list[Any]
|
|
101
|
-
field_groups: list[ModelFieldGroup]
|
|
102
|
-
objects: list[Any]
|
|
103
|
-
batch_size: int = None
|
|
1
|
+
"""
|
|
2
|
+
MTI operation plans - Data structures for multi-table inheritance operations.
|
|
3
|
+
|
|
4
|
+
These are pure data structures returned by MTIHandler to be executed by BulkExecutor.
|
|
5
|
+
This separates planning (logic) from execution (database operations).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from dataclasses import field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ParentLevel:
|
|
15
|
+
"""
|
|
16
|
+
Represents one level in the parent hierarchy for MTI bulk create.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
model_class: The parent model class for this level
|
|
20
|
+
objects: List of parent instances to create
|
|
21
|
+
original_object_map: Maps parent instance id() -> original object id()
|
|
22
|
+
update_conflicts: Whether to enable UPSERT for this level
|
|
23
|
+
unique_fields: Fields for conflict detection (if update_conflicts=True)
|
|
24
|
+
update_fields: Fields to update on conflict (if update_conflicts=True)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
model_class: Any
|
|
28
|
+
objects: list[Any]
|
|
29
|
+
original_object_map: dict[int, int] = field(default_factory=dict)
|
|
30
|
+
update_conflicts: bool = False
|
|
31
|
+
unique_fields: list[str] = field(default_factory=list)
|
|
32
|
+
update_fields: list[str] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class MTICreatePlan:
|
|
37
|
+
"""
|
|
38
|
+
Plan for executing bulk_create on an MTI model.
|
|
39
|
+
|
|
40
|
+
This plan describes WHAT to create, not HOW to create it.
|
|
41
|
+
The executor is responsible for executing this plan.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
inheritance_chain: List of model classes from root to child
|
|
45
|
+
parent_levels: List of ParentLevel objects, one per parent model
|
|
46
|
+
child_objects: List of child instances to create (not yet with parent links)
|
|
47
|
+
child_model: The child model class
|
|
48
|
+
original_objects: Original objects provided by user
|
|
49
|
+
batch_size: Batch size for operations
|
|
50
|
+
existing_record_ids: Set of id() of original objects that represent existing DB records
|
|
51
|
+
update_conflicts: Whether this is an upsert operation
|
|
52
|
+
unique_fields: Fields used for conflict detection (original, unfiltered)
|
|
53
|
+
update_fields: Fields to update on conflict (original, unfiltered)
|
|
54
|
+
child_unique_fields: Pre-filtered field objects for child table conflict detection
|
|
55
|
+
child_update_fields: Pre-filtered field objects for child table updates
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
inheritance_chain: list[Any]
|
|
59
|
+
parent_levels: list[ParentLevel]
|
|
60
|
+
child_objects: list[Any]
|
|
61
|
+
child_model: Any
|
|
62
|
+
original_objects: list[Any]
|
|
63
|
+
batch_size: int = None
|
|
64
|
+
existing_record_ids: set = field(default_factory=set)
|
|
65
|
+
update_conflicts: bool = False
|
|
66
|
+
unique_fields: list[str] = field(default_factory=list)
|
|
67
|
+
update_fields: list[str] = field(default_factory=list)
|
|
68
|
+
child_unique_fields: list = field(default_factory=list) # Field objects for child table
|
|
69
|
+
child_update_fields: list = field(default_factory=list) # Field objects for child table
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class ModelFieldGroup:
|
|
74
|
+
"""
|
|
75
|
+
Represents fields to update for one model in the inheritance chain.
|
|
76
|
+
|
|
77
|
+
Attributes:
|
|
78
|
+
model_class: The model class
|
|
79
|
+
fields: List of field names to update on this model
|
|
80
|
+
filter_field: Field to use for filtering (e.g., 'pk' or parent link attname)
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
model_class: Any
|
|
84
|
+
fields: list[str]
|
|
85
|
+
filter_field: str = "pk"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class MTIUpdatePlan:
|
|
90
|
+
"""
|
|
91
|
+
Plan for executing bulk_update on an MTI model.
|
|
92
|
+
|
|
93
|
+
Attributes:
|
|
94
|
+
inheritance_chain: List of model classes from root to child
|
|
95
|
+
field_groups: List of ModelFieldGroup objects
|
|
96
|
+
objects: Objects to update
|
|
97
|
+
batch_size: Batch size for operations
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
inheritance_chain: list[Any]
|
|
101
|
+
field_groups: list[ModelFieldGroup]
|
|
102
|
+
objects: list[Any]
|
|
103
|
+
batch_size: int = None
|
|
@@ -64,7 +64,7 @@ class RecordClassifier:
|
|
|
64
64
|
# Build lookup dict for this object's unique fields
|
|
65
65
|
lookup = {}
|
|
66
66
|
normalized_values = []
|
|
67
|
-
|
|
67
|
+
|
|
68
68
|
for field_name in unique_fields:
|
|
69
69
|
# Use centralized field value extraction for consistent FK handling
|
|
70
70
|
value = get_field_value_for_db(obj, field_name, query_model)
|
django_bulk_hooks/queryset.py
CHANGED
|
@@ -134,7 +134,11 @@ class HookQuerySet(models.QuerySet):
|
|
|
134
134
|
|
|
135
135
|
@transaction.atomic
|
|
136
136
|
def bulk_delete(
|
|
137
|
-
self,
|
|
137
|
+
self,
|
|
138
|
+
objs,
|
|
139
|
+
bypass_hooks=False,
|
|
140
|
+
bypass_validation=False,
|
|
141
|
+
**kwargs,
|
|
138
142
|
):
|
|
139
143
|
"""
|
|
140
144
|
Delete multiple objects with hook support.
|
django_bulk_hooks/registry.py
CHANGED
|
@@ -116,7 +116,6 @@ class HookRegistry:
|
|
|
116
116
|
if not self._hooks[key]:
|
|
117
117
|
del self._hooks[key]
|
|
118
118
|
|
|
119
|
-
|
|
120
119
|
def clear(self) -> None:
|
|
121
120
|
"""
|
|
122
121
|
Clear all registered hooks.
|
|
@@ -132,7 +131,6 @@ class HookRegistry:
|
|
|
132
131
|
HookMeta._registered.clear()
|
|
133
132
|
HookMeta._class_hook_map.clear()
|
|
134
133
|
|
|
135
|
-
|
|
136
134
|
def list_all(self) -> dict[tuple[type, str], list[HookInfo]]:
|
|
137
135
|
"""
|
|
138
136
|
Get all registered hooks for debugging.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
django_bulk_hooks/__init__.py,sha256=ujBvX-GY4Pg4ACBh7RXm_MOmi2eAowf5s7pG2SXWdpo,2276
|
|
2
|
+
django_bulk_hooks/changeset.py,sha256=qnMD3bR2cNh8ZM8J6ASR5ly5Rjx-tPzXBYkqIjKGW98,6568
|
|
3
|
+
django_bulk_hooks/conditions.py,sha256=ar4pGjtxLKmgSIlO4S6aZFKmaBNchLtxMmWpkn4g9RU,8114
|
|
4
|
+
django_bulk_hooks/constants.py,sha256=PxpEETaO6gdENcTPoXS586lerGKVP3nmjpDvOkmhYxI,509
|
|
5
|
+
django_bulk_hooks/context.py,sha256=mqaC5-yESDTA5ruI7fuXlt8qSgKuOFp0mjq7h1-4HdQ,1926
|
|
6
|
+
django_bulk_hooks/decorators.py,sha256=TdkO4FJyFrVU2zqK6Y_6JjEJ4v3nbKkk7aa22jN10sk,11994
|
|
7
|
+
django_bulk_hooks/dispatcher.py,sha256=yHfmAWChj9rsZwO4jhYDVtJde_Z8bPe9WHRjkYBMfEo,8151
|
|
8
|
+
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
9
|
+
django_bulk_hooks/factory.py,sha256=ezrVM5U023KZqOBbJXb6lYUP-pE7WJmi8Olh2Ew-7RA,18085
|
|
10
|
+
django_bulk_hooks/handler.py,sha256=SRCrMzgolrruTkvMnYBFmXLR-ABiw0JiH3605PEdCZM,4207
|
|
11
|
+
django_bulk_hooks/helpers.py,sha256=tMxUI5oWhbtWByzCCR0Qcj1CgZ6iP5Jfx03EqVmEhxU,7597
|
|
12
|
+
django_bulk_hooks/manager.py,sha256=aDuP87DZLWWbDK2qeA7usl3pxoIjHFIWnQNi_jEq6z0,4446
|
|
13
|
+
django_bulk_hooks/models.py,sha256=TWN_F-SsLGPx9jrkNT9pmJFR5VsZ0Z_QaVOZOmt7bpw,2434
|
|
14
|
+
django_bulk_hooks/operations/__init__.py,sha256=BtJYjmRhe_sScivLsniDaZmBkm0ZLvcmzXFKL7QY2Xg,550
|
|
15
|
+
django_bulk_hooks/operations/analyzer.py,sha256=Pz8mc-EL8KDOfLQFYiRuN-r0OmINW3nIBhRJJCma-yo,10360
|
|
16
|
+
django_bulk_hooks/operations/bulk_executor.py,sha256=po8V_2H3ULiE0RYJ-wbaRIz52SKhss81UHwuQjlz3H8,26214
|
|
17
|
+
django_bulk_hooks/operations/coordinator.py,sha256=NPPkiEIMgbJXEIOtBqvy3OH4l0pOK_NL5jwT7Og9I4o,33765
|
|
18
|
+
django_bulk_hooks/operations/field_utils.py,sha256=cQ9w4xdk-z3PrMLFvRzVV07Wc0D2qbpSepwoupqwQH8,7888
|
|
19
|
+
django_bulk_hooks/operations/mti_handler.py,sha256=Vmz0C0gtYDvbybmb4cDzIaGglSaQK4DQVkaBK-WuQeE,25855
|
|
20
|
+
django_bulk_hooks/operations/mti_plans.py,sha256=HIRJgogHPpm6MV7nZZ-sZhMLUnozpZPV2SzwQHLRzYc,3667
|
|
21
|
+
django_bulk_hooks/operations/record_classifier.py,sha256=It85hJC2K-UsEOLbTR-QBdY5UPV-acQIJ91TSGa7pYo,7053
|
|
22
|
+
django_bulk_hooks/queryset.py,sha256=g_9OtOTC8FXY0hBwYr2FCqQ3mYXbfJTFPLlFV3SHmWQ,5600
|
|
23
|
+
django_bulk_hooks/registry.py,sha256=4HxP1mVK2z4VzvlohbEw2359wM21UJZJYagJJ1komM0,7947
|
|
24
|
+
django_bulk_hooks-0.2.62.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
25
|
+
django_bulk_hooks-0.2.62.dist-info/METADATA,sha256=Ib--glZc6N4AtvIcIGqMAQUTdAk0AeCmrV2wv6u8iWQ,9265
|
|
26
|
+
django_bulk_hooks-0.2.62.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
27
|
+
django_bulk_hooks-0.2.62.dist-info/RECORD,,
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
django_bulk_hooks/__init__.py,sha256=ujBvX-GY4Pg4ACBh7RXm_MOmi2eAowf5s7pG2SXWdpo,2276
|
|
2
|
-
django_bulk_hooks/changeset.py,sha256=opWqb_x-_9KaMsUJc7lI3wScWLHn2Yhe7hB-ngFCBp0,6731
|
|
3
|
-
django_bulk_hooks/conditions.py,sha256=v2DMFmWI7bppBQw5qdbO5CmQRN_QtUwnBjcyKBJLLbw,8030
|
|
4
|
-
django_bulk_hooks/constants.py,sha256=PxpEETaO6gdENcTPoXS586lerGKVP3nmjpDvOkmhYxI,509
|
|
5
|
-
django_bulk_hooks/context.py,sha256=mqaC5-yESDTA5ruI7fuXlt8qSgKuOFp0mjq7h1-4HdQ,1926
|
|
6
|
-
django_bulk_hooks/decorators.py,sha256=P7cvzFgORJRW-YQHNAxNXqQOP9OywBmA7Rz9kiJoxUk,12237
|
|
7
|
-
django_bulk_hooks/dispatcher.py,sha256=CiKYe5ecUPu5TYUZq8ToaRT40TkLc5l5mczgf5XDzGA,8217
|
|
8
|
-
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
9
|
-
django_bulk_hooks/factory.py,sha256=ezrVM5U023KZqOBbJXb6lYUP-pE7WJmi8Olh2Ew-7RA,18085
|
|
10
|
-
django_bulk_hooks/handler.py,sha256=38ejMdQ9reYA07_XQ9tC8xv0lW3amO-m8gPzuRNOyj0,4200
|
|
11
|
-
django_bulk_hooks/helpers.py,sha256=e14aYE1lKaj8-krFclY_WfJF2uWQpblfsl5hqsW-dxY,7800
|
|
12
|
-
django_bulk_hooks/manager.py,sha256=g11g1MZ4DJGIM4prYLpYLejTsz0YkYPWeoxWA4dcgYk,4596
|
|
13
|
-
django_bulk_hooks/models.py,sha256=9uh7leV3EEnTWkNKNqT1xevamPTczRhW7KbnIHraBlk,2969
|
|
14
|
-
django_bulk_hooks/operations/__init__.py,sha256=BtJYjmRhe_sScivLsniDaZmBkm0ZLvcmzXFKL7QY2Xg,550
|
|
15
|
-
django_bulk_hooks/operations/analyzer.py,sha256=wO-LUgGExE8y3fb25kdfpbj_KdSIW8fd4QFp0Os8Muk,10679
|
|
16
|
-
django_bulk_hooks/operations/bulk_executor.py,sha256=byo09_65qQI_Z-7HvIoeWh-fRAFnI6qrWFjDr2Ar8LA,26275
|
|
17
|
-
django_bulk_hooks/operations/coordinator.py,sha256=d7DD_CMUA4yeUJeJueBOQ9LrJFFj3emVx56hG1Ju5Xg,33787
|
|
18
|
-
django_bulk_hooks/operations/field_utils.py,sha256=M1HfBj5EK5c6SbOmkVT9s6u_SEOh_N4bheytm5jBD4o,7980
|
|
19
|
-
django_bulk_hooks/operations/mti_handler.py,sha256=LbmbAzowfaQePWjjryHJxogvyDZ4GTL176gc6ezKVYA,25829
|
|
20
|
-
django_bulk_hooks/operations/mti_plans.py,sha256=Vl0lV7AuhmovI0_qcD73KairyPy73l36fJYk8wRBh2g,3770
|
|
21
|
-
django_bulk_hooks/operations/record_classifier.py,sha256=kqML4aO11X9K3SSJ5DUlUukwI172j_Tk12Kr77ee8q8,7065
|
|
22
|
-
django_bulk_hooks/queryset.py,sha256=8xdA3jV6SeEGzW-av346I85Kq1N1uqt178aEh8vm8v8,5568
|
|
23
|
-
django_bulk_hooks/registry.py,sha256=uum5jhGI3TPaoiXuA1MdBdu4gbE3rQGGwQ5YDjiMcjk,7949
|
|
24
|
-
django_bulk_hooks-0.2.61.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
25
|
-
django_bulk_hooks-0.2.61.dist-info/METADATA,sha256=UxXjcApyuPWulCJWCm9LJGeI6Krh7bwb8mdOsVMfmc0,9265
|
|
26
|
-
django_bulk_hooks-0.2.61.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
27
|
-
django_bulk_hooks-0.2.61.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|