django-bulk-hooks 0.1.187__py3-none-any.whl → 0.1.188__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/queryset.py +729 -705
- {django_bulk_hooks-0.1.187.dist-info → django_bulk_hooks-0.1.188.dist-info}/METADATA +1 -1
- {django_bulk_hooks-0.1.187.dist-info → django_bulk_hooks-0.1.188.dist-info}/RECORD +5 -5
- {django_bulk_hooks-0.1.187.dist-info → django_bulk_hooks-0.1.188.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.187.dist-info → django_bulk_hooks-0.1.188.dist-info}/WHEEL +0 -0
django_bulk_hooks/queryset.py
CHANGED
|
@@ -1,705 +1,729 @@
|
|
|
1
|
-
from django.db import models, transaction
|
|
2
|
-
from django.db.models import AutoField
|
|
3
|
-
|
|
4
|
-
from django_bulk_hooks import engine
|
|
5
|
-
from django_bulk_hooks.constants import (
|
|
6
|
-
AFTER_CREATE,
|
|
7
|
-
AFTER_DELETE,
|
|
8
|
-
AFTER_UPDATE,
|
|
9
|
-
BEFORE_CREATE,
|
|
10
|
-
BEFORE_DELETE,
|
|
11
|
-
BEFORE_UPDATE,
|
|
12
|
-
VALIDATE_CREATE,
|
|
13
|
-
VALIDATE_DELETE,
|
|
14
|
-
VALIDATE_UPDATE,
|
|
15
|
-
)
|
|
16
|
-
from django_bulk_hooks.context import HookContext
|
|
17
|
-
from django.db.models import When, Value, Case
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class HookQuerySet(models.QuerySet):
|
|
21
|
-
@transaction.atomic
|
|
22
|
-
def delete(self):
|
|
23
|
-
objs = list(self)
|
|
24
|
-
if not objs:
|
|
25
|
-
return 0
|
|
26
|
-
|
|
27
|
-
model_cls = self.model
|
|
28
|
-
ctx = HookContext(model_cls)
|
|
29
|
-
|
|
30
|
-
# Run validation hooks first
|
|
31
|
-
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
32
|
-
|
|
33
|
-
# Then run business logic hooks
|
|
34
|
-
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
35
|
-
|
|
36
|
-
# Use Django's standard delete() method
|
|
37
|
-
result = super().delete()
|
|
38
|
-
|
|
39
|
-
# Run AFTER_DELETE hooks
|
|
40
|
-
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
41
|
-
|
|
42
|
-
return result
|
|
43
|
-
|
|
44
|
-
@transaction.atomic
|
|
45
|
-
def update(self, **kwargs):
|
|
46
|
-
instances = list(self)
|
|
47
|
-
if not instances:
|
|
48
|
-
return 0
|
|
49
|
-
|
|
50
|
-
model_cls = self.model
|
|
51
|
-
pks = [obj.pk for obj in instances]
|
|
52
|
-
|
|
53
|
-
# Load originals for hook comparison and ensure they match the order of instances
|
|
54
|
-
# Use the base manager to avoid recursion
|
|
55
|
-
original_map = {
|
|
56
|
-
obj.pk: obj for obj in model_cls._base_manager.filter(pk__in=pks)
|
|
57
|
-
}
|
|
58
|
-
originals = [original_map.get(obj.pk) for obj in instances]
|
|
59
|
-
|
|
60
|
-
# Apply field updates to instances
|
|
61
|
-
for obj in instances:
|
|
62
|
-
for field, value in kwargs.items():
|
|
63
|
-
setattr(obj, field, value)
|
|
64
|
-
|
|
65
|
-
# Run BEFORE_UPDATE hooks
|
|
66
|
-
ctx = HookContext(model_cls)
|
|
67
|
-
engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
|
|
68
|
-
|
|
69
|
-
# Use Django's built-in update logic directly
|
|
70
|
-
# Call the base QuerySet implementation to avoid recursion
|
|
71
|
-
update_count = super().update(**kwargs)
|
|
72
|
-
|
|
73
|
-
# Run AFTER_UPDATE hooks
|
|
74
|
-
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
75
|
-
|
|
76
|
-
return update_count
|
|
77
|
-
|
|
78
|
-
@transaction.atomic
|
|
79
|
-
def bulk_create(
|
|
80
|
-
self,
|
|
81
|
-
objs,
|
|
82
|
-
batch_size=None,
|
|
83
|
-
ignore_conflicts=False,
|
|
84
|
-
update_conflicts=False,
|
|
85
|
-
update_fields=None,
|
|
86
|
-
unique_fields=None,
|
|
87
|
-
bypass_hooks=False,
|
|
88
|
-
bypass_validation=False,
|
|
89
|
-
):
|
|
90
|
-
"""
|
|
91
|
-
Insert each of the instances into the database. Behaves like Django's bulk_create,
|
|
92
|
-
but supports multi-table inheritance (MTI) models and hooks. All arguments are supported and
|
|
93
|
-
passed through to the correct logic. For MTI, only a subset of options may be supported.
|
|
94
|
-
"""
|
|
95
|
-
model_cls = self.model
|
|
96
|
-
|
|
97
|
-
# When you bulk insert you don't get the primary keys back (if it's an
|
|
98
|
-
# autoincrement, except if can_return_rows_from_bulk_insert=True), so
|
|
99
|
-
# you can't insert into the child tables which references this. There
|
|
100
|
-
# are two workarounds:
|
|
101
|
-
# 1) This could be implemented if you didn't have an autoincrement pk
|
|
102
|
-
# 2) You could do it by doing O(n) normal inserts into the parent
|
|
103
|
-
# tables to get the primary keys back and then doing a single bulk
|
|
104
|
-
# insert into the childmost table.
|
|
105
|
-
# We currently set the primary keys on the objects when using
|
|
106
|
-
# PostgreSQL via the RETURNING ID clause. It should be possible for
|
|
107
|
-
# Oracle as well, but the semantics for extracting the primary keys is
|
|
108
|
-
# trickier so it's not done yet.
|
|
109
|
-
if batch_size is not None and batch_size <= 0:
|
|
110
|
-
raise ValueError("Batch size must be a positive integer.")
|
|
111
|
-
|
|
112
|
-
if not objs:
|
|
113
|
-
return objs
|
|
114
|
-
|
|
115
|
-
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
116
|
-
raise TypeError(
|
|
117
|
-
f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
# Check for MTI - if we detect multi-table inheritance, we need special handling
|
|
121
|
-
# This follows Django's approach: check that the parents share the same concrete model
|
|
122
|
-
# with our model to detect the inheritance pattern ConcreteGrandParent ->
|
|
123
|
-
# MultiTableParent -> ProxyChild. Simply checking self.model._meta.proxy would not
|
|
124
|
-
# identify that case as involving multiple tables.
|
|
125
|
-
is_mti = False
|
|
126
|
-
for parent in model_cls._meta.all_parents:
|
|
127
|
-
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
128
|
-
is_mti = True
|
|
129
|
-
break
|
|
130
|
-
|
|
131
|
-
# Fire hooks before DB ops
|
|
132
|
-
if not bypass_hooks:
|
|
133
|
-
ctx = HookContext(model_cls)
|
|
134
|
-
if not bypass_validation:
|
|
135
|
-
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
136
|
-
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
137
|
-
|
|
138
|
-
# For MTI models, we need to handle them specially
|
|
139
|
-
if is_mti:
|
|
140
|
-
# Use our MTI-specific logic
|
|
141
|
-
# Filter out custom parameters that Django's bulk_create doesn't accept
|
|
142
|
-
mti_kwargs = {
|
|
143
|
-
"batch_size": batch_size,
|
|
144
|
-
"ignore_conflicts": ignore_conflicts,
|
|
145
|
-
"update_conflicts": update_conflicts,
|
|
146
|
-
"update_fields": update_fields,
|
|
147
|
-
"unique_fields": unique_fields,
|
|
148
|
-
}
|
|
149
|
-
# Remove custom hook kwargs if present in self.bulk_create signature
|
|
150
|
-
result = self._mti_bulk_create(
|
|
151
|
-
objs,
|
|
152
|
-
**mti_kwargs,
|
|
153
|
-
)
|
|
154
|
-
else:
|
|
155
|
-
# For single-table models, use Django's built-in bulk_create
|
|
156
|
-
# but we need to call it on the base manager to avoid recursion
|
|
157
|
-
# Filter out custom parameters that Django's bulk_create doesn't accept
|
|
158
|
-
|
|
159
|
-
result = super().bulk_create(
|
|
160
|
-
objs,
|
|
161
|
-
batch_size=batch_size,
|
|
162
|
-
ignore_conflicts=ignore_conflicts,
|
|
163
|
-
update_conflicts=update_conflicts,
|
|
164
|
-
update_fields=update_fields,
|
|
165
|
-
unique_fields=unique_fields,
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
# Fire AFTER_CREATE hooks
|
|
169
|
-
if not bypass_hooks:
|
|
170
|
-
engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
|
|
171
|
-
|
|
172
|
-
return result
|
|
173
|
-
|
|
174
|
-
@transaction.atomic
|
|
175
|
-
def bulk_update(
|
|
176
|
-
self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
177
|
-
):
|
|
178
|
-
"""
|
|
179
|
-
Bulk update objects in the database with MTI support.
|
|
180
|
-
"""
|
|
181
|
-
print(f"\n=== BULK UPDATE DEBUG ===")
|
|
182
|
-
print(f"Model: {self.model.__name__}")
|
|
183
|
-
print(f"Number of objects: {len(objs)}")
|
|
184
|
-
print(f"Fields: {fields}")
|
|
185
|
-
print(f"Bypass hooks: {bypass_hooks}")
|
|
186
|
-
print(f"Bypass validation: {bypass_validation}")
|
|
187
|
-
|
|
188
|
-
model_cls = self.model
|
|
189
|
-
|
|
190
|
-
if not objs:
|
|
191
|
-
print("No objects to update")
|
|
192
|
-
return []
|
|
193
|
-
|
|
194
|
-
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
195
|
-
raise TypeError(
|
|
196
|
-
f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
# Check for MTI
|
|
200
|
-
is_mti = False
|
|
201
|
-
for parent in model_cls._meta.all_parents:
|
|
202
|
-
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
203
|
-
is_mti = True
|
|
204
|
-
break
|
|
205
|
-
|
|
206
|
-
print(f"Is MTI: {is_mti}")
|
|
207
|
-
print(f"Model concrete model: {model_cls._meta.concrete_model.__name__}")
|
|
208
|
-
for parent in model_cls._meta.all_parents:
|
|
209
|
-
print(f" Parent {parent.__name__}: concrete_model = {parent._meta.concrete_model.__name__}")
|
|
210
|
-
|
|
211
|
-
if not bypass_hooks:
|
|
212
|
-
# Load originals for hook comparison
|
|
213
|
-
original_map = {
|
|
214
|
-
obj.pk: obj
|
|
215
|
-
for obj in model_cls._base_manager.filter(
|
|
216
|
-
pk__in=[obj.pk for obj in objs]
|
|
217
|
-
)
|
|
218
|
-
}
|
|
219
|
-
originals = [original_map.get(obj.pk) for obj in objs]
|
|
220
|
-
|
|
221
|
-
ctx = HookContext(model_cls)
|
|
222
|
-
|
|
223
|
-
# Run validation hooks first
|
|
224
|
-
if not bypass_validation:
|
|
225
|
-
engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
|
|
226
|
-
|
|
227
|
-
# Then run business logic hooks
|
|
228
|
-
engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
|
|
229
|
-
|
|
230
|
-
# Detect modified fields during hooks
|
|
231
|
-
modified_fields = self._detect_modified_fields(objs, originals)
|
|
232
|
-
if modified_fields:
|
|
233
|
-
fields_set = set(fields)
|
|
234
|
-
fields_set.update(modified_fields)
|
|
235
|
-
fields = list(fields_set)
|
|
236
|
-
print(f"Modified fields detected: {modified_fields}")
|
|
237
|
-
|
|
238
|
-
# Handle MTI models differently
|
|
239
|
-
if is_mti:
|
|
240
|
-
print("Using MTI bulk update logic")
|
|
241
|
-
result = self._mti_bulk_update(objs, fields, **kwargs)
|
|
242
|
-
else:
|
|
243
|
-
print("Using standard Django bulk_update")
|
|
244
|
-
# For single-table models, use Django's built-in bulk_update
|
|
245
|
-
django_kwargs = {
|
|
246
|
-
k: v
|
|
247
|
-
for k, v in kwargs.items()
|
|
248
|
-
if k not in ["bypass_hooks", "bypass_validation"]
|
|
249
|
-
}
|
|
250
|
-
result = super().bulk_update(objs, fields, **django_kwargs)
|
|
251
|
-
|
|
252
|
-
if not bypass_hooks:
|
|
253
|
-
engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
|
|
254
|
-
|
|
255
|
-
print(f"Bulk update result: {result}")
|
|
256
|
-
return result
|
|
257
|
-
|
|
258
|
-
def _detect_modified_fields(self, new_instances, original_instances):
|
|
259
|
-
"""
|
|
260
|
-
Detect fields that were modified during BEFORE_UPDATE hooks by comparing
|
|
261
|
-
new instances with their original values.
|
|
262
|
-
"""
|
|
263
|
-
if not original_instances:
|
|
264
|
-
return set()
|
|
265
|
-
|
|
266
|
-
modified_fields = set()
|
|
267
|
-
|
|
268
|
-
# Since original_instances is now ordered to match new_instances, we can zip them directly
|
|
269
|
-
for new_instance, original in zip(new_instances, original_instances):
|
|
270
|
-
if new_instance.pk is None or original is None:
|
|
271
|
-
continue
|
|
272
|
-
|
|
273
|
-
# Compare all fields to detect changes
|
|
274
|
-
for field in new_instance._meta.fields:
|
|
275
|
-
if field.name == "id":
|
|
276
|
-
continue
|
|
277
|
-
|
|
278
|
-
new_value = getattr(new_instance, field.name)
|
|
279
|
-
original_value = getattr(original, field.name)
|
|
280
|
-
|
|
281
|
-
# Handle different field types appropriately
|
|
282
|
-
if field.is_relation:
|
|
283
|
-
# For foreign keys, compare the pk values
|
|
284
|
-
new_pk = new_value.pk if new_value else None
|
|
285
|
-
original_pk = original_value.pk if original_value else None
|
|
286
|
-
if new_pk != original_pk:
|
|
287
|
-
modified_fields.add(field.name)
|
|
288
|
-
else:
|
|
289
|
-
# For regular fields, use direct comparison
|
|
290
|
-
if new_value != original_value:
|
|
291
|
-
modified_fields.add(field.name)
|
|
292
|
-
|
|
293
|
-
return modified_fields
|
|
294
|
-
|
|
295
|
-
def _get_inheritance_chain(self):
|
|
296
|
-
"""
|
|
297
|
-
Get the complete inheritance chain from root parent to current model.
|
|
298
|
-
Returns list of model classes in order: [RootParent, Parent, Child]
|
|
299
|
-
"""
|
|
300
|
-
print(f"\n=== GET INHERITANCE CHAIN DEBUG ===")
|
|
301
|
-
print(f"Current model: {self.model.__name__}")
|
|
302
|
-
|
|
303
|
-
chain = []
|
|
304
|
-
current_model = self.model
|
|
305
|
-
while current_model:
|
|
306
|
-
print(f"Processing model: {current_model.__name__}")
|
|
307
|
-
if not current_model._meta.proxy:
|
|
308
|
-
chain.append(current_model)
|
|
309
|
-
print(f" Added to chain: {current_model.__name__}")
|
|
310
|
-
else:
|
|
311
|
-
print(f" Skipped proxy model: {current_model.__name__}")
|
|
312
|
-
|
|
313
|
-
parents = [
|
|
314
|
-
parent
|
|
315
|
-
for parent in current_model._meta.parents.keys()
|
|
316
|
-
if not parent._meta.proxy
|
|
317
|
-
]
|
|
318
|
-
print(f" Parents: {[p.__name__ for p in parents]}")
|
|
319
|
-
current_model = parents[0] if parents else None
|
|
320
|
-
|
|
321
|
-
chain.reverse()
|
|
322
|
-
print(f"Final inheritance chain: {[m.__name__ for m in chain]}")
|
|
323
|
-
return chain
|
|
324
|
-
|
|
325
|
-
def _mti_bulk_create(self, objs, inheritance_chain=None, **kwargs):
|
|
326
|
-
"""
|
|
327
|
-
Implements Django's suggested workaround #2 for MTI bulk_create:
|
|
328
|
-
O(n) normal inserts into parent tables to get primary keys back,
|
|
329
|
-
then single bulk insert into childmost table.
|
|
330
|
-
Sets auto_now_add/auto_now fields for each model in the chain.
|
|
331
|
-
"""
|
|
332
|
-
# Remove custom hook kwargs before passing to Django internals
|
|
333
|
-
django_kwargs = {
|
|
334
|
-
k: v
|
|
335
|
-
for k, v in kwargs.items()
|
|
336
|
-
if k not in ["bypass_hooks", "bypass_validation"]
|
|
337
|
-
}
|
|
338
|
-
if inheritance_chain is None:
|
|
339
|
-
inheritance_chain = self._get_inheritance_chain()
|
|
340
|
-
|
|
341
|
-
# Safety check to prevent infinite recursion
|
|
342
|
-
if len(inheritance_chain) > 10: # Arbitrary limit to prevent infinite loops
|
|
343
|
-
raise ValueError(
|
|
344
|
-
"Inheritance chain too deep - possible infinite recursion detected"
|
|
345
|
-
)
|
|
346
|
-
|
|
347
|
-
batch_size = django_kwargs.get("batch_size") or len(objs)
|
|
348
|
-
created_objects = []
|
|
349
|
-
with transaction.atomic(using=self.db, savepoint=False):
|
|
350
|
-
for i in range(0, len(objs), batch_size):
|
|
351
|
-
batch = objs[i : i + batch_size]
|
|
352
|
-
batch_result = self._process_mti_bulk_create_batch(
|
|
353
|
-
batch, inheritance_chain, **django_kwargs
|
|
354
|
-
)
|
|
355
|
-
created_objects.extend(batch_result)
|
|
356
|
-
return created_objects
|
|
357
|
-
|
|
358
|
-
def _process_mti_bulk_create_batch(self, batch, inheritance_chain, **kwargs):
|
|
359
|
-
"""
|
|
360
|
-
Process a single batch of objects through the inheritance chain.
|
|
361
|
-
Implements Django's suggested workaround #2: O(n) normal inserts into parent
|
|
362
|
-
tables to get primary keys back, then single bulk insert into childmost table.
|
|
363
|
-
"""
|
|
364
|
-
# For MTI, we need to save parent objects first to get PKs
|
|
365
|
-
# Then we can use Django's bulk_create for the child objects
|
|
366
|
-
parent_objects_map = {}
|
|
367
|
-
|
|
368
|
-
# Step 1: Do O(n) normal inserts into parent tables to get primary keys back
|
|
369
|
-
# Get bypass_hooks from kwargs
|
|
370
|
-
bypass_hooks = kwargs.get("bypass_hooks", False)
|
|
371
|
-
bypass_validation = kwargs.get("bypass_validation", False)
|
|
372
|
-
|
|
373
|
-
for obj in batch:
|
|
374
|
-
parent_instances = {}
|
|
375
|
-
current_parent = None
|
|
376
|
-
for model_class in inheritance_chain[:-1]:
|
|
377
|
-
parent_obj = self._create_parent_instance(
|
|
378
|
-
obj, model_class, current_parent
|
|
379
|
-
)
|
|
380
|
-
|
|
381
|
-
# Fire parent hooks if not bypassed
|
|
382
|
-
if not bypass_hooks:
|
|
383
|
-
ctx = HookContext(model_class)
|
|
384
|
-
if not bypass_validation:
|
|
385
|
-
engine.run(model_class, VALIDATE_CREATE, [parent_obj], ctx=ctx)
|
|
386
|
-
engine.run(model_class, BEFORE_CREATE, [parent_obj], ctx=ctx)
|
|
387
|
-
|
|
388
|
-
# Use Django's base manager to create the object and get PKs back
|
|
389
|
-
# This bypasses hooks and the MTI exception
|
|
390
|
-
field_values = {
|
|
391
|
-
field.name: getattr(parent_obj, field.name)
|
|
392
|
-
for field in model_class._meta.local_fields
|
|
393
|
-
if hasattr(parent_obj, field.name)
|
|
394
|
-
and getattr(parent_obj, field.name) is not None
|
|
395
|
-
}
|
|
396
|
-
created_obj = model_class._base_manager.using(self.db).create(
|
|
397
|
-
**field_values
|
|
398
|
-
)
|
|
399
|
-
|
|
400
|
-
# Update the parent_obj with the created object's PK
|
|
401
|
-
parent_obj.pk = created_obj.pk
|
|
402
|
-
parent_obj._state.adding = False
|
|
403
|
-
parent_obj._state.db = self.db
|
|
404
|
-
|
|
405
|
-
# Fire AFTER_CREATE hooks for parent
|
|
406
|
-
if not bypass_hooks:
|
|
407
|
-
engine.run(model_class, AFTER_CREATE, [parent_obj], ctx=ctx)
|
|
408
|
-
|
|
409
|
-
parent_instances[model_class] = parent_obj
|
|
410
|
-
current_parent = parent_obj
|
|
411
|
-
parent_objects_map[id(obj)] = parent_instances
|
|
412
|
-
|
|
413
|
-
# Step 2: Create all child objects and do single bulk insert into childmost table
|
|
414
|
-
child_model = inheritance_chain[-1]
|
|
415
|
-
all_child_objects = []
|
|
416
|
-
for obj in batch:
|
|
417
|
-
child_obj = self._create_child_instance(
|
|
418
|
-
obj, child_model, parent_objects_map.get(id(obj), {})
|
|
419
|
-
)
|
|
420
|
-
all_child_objects.append(child_obj)
|
|
421
|
-
|
|
422
|
-
# Step 2.5: Use Django's internal bulk_create infrastructure
|
|
423
|
-
if all_child_objects:
|
|
424
|
-
# Get the base manager's queryset
|
|
425
|
-
base_qs = child_model._base_manager.using(self.db)
|
|
426
|
-
|
|
427
|
-
# Use Django's exact approach: call _prepare_for_bulk_create then partition
|
|
428
|
-
base_qs._prepare_for_bulk_create(all_child_objects)
|
|
429
|
-
|
|
430
|
-
# Implement our own partition since itertools.partition might not be available
|
|
431
|
-
objs_without_pk, objs_with_pk = [], []
|
|
432
|
-
for obj in all_child_objects:
|
|
433
|
-
if obj._is_pk_set():
|
|
434
|
-
objs_with_pk.append(obj)
|
|
435
|
-
else:
|
|
436
|
-
objs_without_pk.append(obj)
|
|
437
|
-
|
|
438
|
-
# Use Django's internal _batched_insert method
|
|
439
|
-
opts = child_model._meta
|
|
440
|
-
# For child models in MTI, we need to include the foreign key to the parent
|
|
441
|
-
# but exclude the primary key since it's inherited
|
|
442
|
-
|
|
443
|
-
# Include all local fields except generated ones
|
|
444
|
-
# We need to include the foreign key to the parent (business_ptr)
|
|
445
|
-
fields = [f for f in opts.local_fields if not f.generated]
|
|
446
|
-
|
|
447
|
-
with transaction.atomic(using=self.db, savepoint=False):
|
|
448
|
-
if objs_with_pk:
|
|
449
|
-
returned_columns = base_qs._batched_insert(
|
|
450
|
-
objs_with_pk,
|
|
451
|
-
fields,
|
|
452
|
-
batch_size=len(objs_with_pk), # Use actual batch size
|
|
453
|
-
)
|
|
454
|
-
for obj_with_pk, results in zip(objs_with_pk, returned_columns):
|
|
455
|
-
for result, field in zip(results, opts.db_returning_fields):
|
|
456
|
-
if field != opts.pk:
|
|
457
|
-
setattr(obj_with_pk, field.attname, result)
|
|
458
|
-
for obj_with_pk in objs_with_pk:
|
|
459
|
-
obj_with_pk._state.adding = False
|
|
460
|
-
obj_with_pk._state.db = self.db
|
|
461
|
-
|
|
462
|
-
if objs_without_pk:
|
|
463
|
-
# For objects without PK, we still need to exclude primary key fields
|
|
464
|
-
fields = [
|
|
465
|
-
f
|
|
466
|
-
for f in fields
|
|
467
|
-
if not isinstance(f, AutoField) and not f.primary_key
|
|
468
|
-
]
|
|
469
|
-
returned_columns = base_qs._batched_insert(
|
|
470
|
-
objs_without_pk,
|
|
471
|
-
fields,
|
|
472
|
-
batch_size=len(objs_without_pk), # Use actual batch size
|
|
473
|
-
)
|
|
474
|
-
for obj_without_pk, results in zip(
|
|
475
|
-
objs_without_pk, returned_columns
|
|
476
|
-
):
|
|
477
|
-
for result, field in zip(results, opts.db_returning_fields):
|
|
478
|
-
setattr(obj_without_pk, field.attname, result)
|
|
479
|
-
obj_without_pk._state.adding = False
|
|
480
|
-
obj_without_pk._state.db = self.db
|
|
481
|
-
|
|
482
|
-
# Step 3: Update original objects with generated PKs and state
|
|
483
|
-
pk_field_name = child_model._meta.pk.name
|
|
484
|
-
for orig_obj, child_obj in zip(batch, all_child_objects):
|
|
485
|
-
child_pk = getattr(child_obj, pk_field_name)
|
|
486
|
-
setattr(orig_obj, pk_field_name, child_pk)
|
|
487
|
-
orig_obj._state.adding = False
|
|
488
|
-
orig_obj._state.db = self.db
|
|
489
|
-
|
|
490
|
-
return batch
|
|
491
|
-
|
|
492
|
-
def _create_parent_instance(self, source_obj, parent_model, current_parent):
|
|
493
|
-
parent_obj = parent_model()
|
|
494
|
-
for field in parent_model._meta.local_fields:
|
|
495
|
-
# Only copy if the field exists on the source and is not None
|
|
496
|
-
if hasattr(source_obj, field.name):
|
|
497
|
-
value = getattr(source_obj, field.name, None)
|
|
498
|
-
if value is not None:
|
|
499
|
-
setattr(parent_obj, field.name, value)
|
|
500
|
-
if current_parent is not None:
|
|
501
|
-
for field in parent_model._meta.local_fields:
|
|
502
|
-
if (
|
|
503
|
-
hasattr(field, "remote_field")
|
|
504
|
-
and field.remote_field
|
|
505
|
-
and field.remote_field.model == current_parent.__class__
|
|
506
|
-
):
|
|
507
|
-
setattr(parent_obj, field.name, current_parent)
|
|
508
|
-
break
|
|
509
|
-
|
|
510
|
-
# Handle auto_now_add and auto_now fields like Django does
|
|
511
|
-
for field in parent_model._meta.local_fields:
|
|
512
|
-
if hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
513
|
-
# Ensure auto_now_add fields are properly set
|
|
514
|
-
if getattr(parent_obj, field.name) is None:
|
|
515
|
-
field.pre_save(parent_obj, add=True)
|
|
516
|
-
# Explicitly set the value to ensure it's not None
|
|
517
|
-
setattr(parent_obj, field.name, field.value_from_object(parent_obj))
|
|
518
|
-
elif hasattr(field, "auto_now") and field.auto_now:
|
|
519
|
-
field.pre_save(parent_obj, add=True)
|
|
520
|
-
|
|
521
|
-
return parent_obj
|
|
522
|
-
|
|
523
|
-
def _create_child_instance(self, source_obj, child_model, parent_instances):
|
|
524
|
-
child_obj = child_model()
|
|
525
|
-
# Only copy fields that exist in the child model's local fields
|
|
526
|
-
for field in child_model._meta.local_fields:
|
|
527
|
-
if isinstance(field, AutoField):
|
|
528
|
-
continue
|
|
529
|
-
if hasattr(source_obj, field.name):
|
|
530
|
-
value = getattr(source_obj, field.name, None)
|
|
531
|
-
if value is not None:
|
|
532
|
-
setattr(child_obj, field.name, value)
|
|
533
|
-
|
|
534
|
-
# Set parent links for MTI
|
|
535
|
-
for parent_model, parent_instance in parent_instances.items():
|
|
536
|
-
parent_link = child_model._meta.get_ancestor_link(parent_model)
|
|
537
|
-
if parent_link:
|
|
538
|
-
# Set both the foreign key value (the ID) and the object reference
|
|
539
|
-
# This follows Django's pattern in _set_pk_val
|
|
540
|
-
setattr(
|
|
541
|
-
child_obj, parent_link.attname, parent_instance.pk
|
|
542
|
-
) # Set the foreign key value
|
|
543
|
-
setattr(
|
|
544
|
-
child_obj, parent_link.name, parent_instance
|
|
545
|
-
) # Set the object reference
|
|
546
|
-
|
|
547
|
-
# Handle auto_now_add and auto_now fields like Django does
|
|
548
|
-
for field in child_model._meta.local_fields:
|
|
549
|
-
if hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
550
|
-
# Ensure auto_now_add fields are properly set
|
|
551
|
-
if getattr(child_obj, field.name) is None:
|
|
552
|
-
field.pre_save(child_obj, add=True)
|
|
553
|
-
# Explicitly set the value to ensure it's not None
|
|
554
|
-
setattr(child_obj, field.name, field.value_from_object(child_obj))
|
|
555
|
-
elif hasattr(field, "auto_now") and field.auto_now:
|
|
556
|
-
field.pre_save(child_obj, add=True)
|
|
557
|
-
|
|
558
|
-
return child_obj
|
|
559
|
-
|
|
560
|
-
def _mti_bulk_update(self, objs, fields, **kwargs):
|
|
561
|
-
"""
|
|
562
|
-
Custom bulk update implementation for MTI models.
|
|
563
|
-
Updates each table in the inheritance chain efficiently using Django's batch_size.
|
|
564
|
-
"""
|
|
565
|
-
print(f"\n=== MTI BULK UPDATE DEBUG ===")
|
|
566
|
-
print(f"Model: {self.model.__name__}")
|
|
567
|
-
print(f"Number of objects: {len(objs)}")
|
|
568
|
-
print(f"Fields to update: {fields}")
|
|
569
|
-
|
|
570
|
-
model_cls = self.model
|
|
571
|
-
inheritance_chain = self._get_inheritance_chain()
|
|
572
|
-
print(f"Inheritance chain: {[m.__name__ for m in inheritance_chain]}")
|
|
573
|
-
|
|
574
|
-
# Remove custom hook kwargs before passing to Django internals
|
|
575
|
-
django_kwargs = {
|
|
576
|
-
k: v
|
|
577
|
-
for k, v in kwargs.items()
|
|
578
|
-
if k not in ["bypass_hooks", "bypass_validation"]
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
# Safety check to prevent infinite recursion
|
|
582
|
-
if len(inheritance_chain) > 10: # Arbitrary limit to prevent infinite loops
|
|
583
|
-
raise ValueError(
|
|
584
|
-
"Inheritance chain too deep - possible infinite recursion detected"
|
|
585
|
-
)
|
|
586
|
-
|
|
587
|
-
# Group fields by model in the inheritance chain
|
|
588
|
-
field_groups = {}
|
|
589
|
-
for field_name in fields:
|
|
590
|
-
field = model_cls._meta.get_field(field_name)
|
|
591
|
-
# Find which model in the inheritance chain this field belongs to
|
|
592
|
-
for model in inheritance_chain:
|
|
593
|
-
if field in model._meta.local_fields:
|
|
594
|
-
if model not in field_groups:
|
|
595
|
-
field_groups[model] = []
|
|
596
|
-
field_groups[model].append(field_name)
|
|
597
|
-
print(f"Field '{field_name}' belongs to model '{model.__name__}'")
|
|
598
|
-
break
|
|
599
|
-
|
|
600
|
-
print(f"Field groups: {field_groups}")
|
|
601
|
-
|
|
602
|
-
# Process in batches
|
|
603
|
-
batch_size = django_kwargs.get("batch_size") or len(objs)
|
|
604
|
-
total_updated = 0
|
|
605
|
-
|
|
606
|
-
print(f"Processing in batches of size: {batch_size}")
|
|
607
|
-
|
|
608
|
-
with transaction.atomic(using=self.db, savepoint=False):
|
|
609
|
-
for i in range(0, len(objs), batch_size):
|
|
610
|
-
batch = objs[i : i + batch_size]
|
|
611
|
-
print(f"\n--- Processing batch {i//batch_size + 1} ({len(batch)} objects) ---")
|
|
612
|
-
batch_result = self._process_mti_bulk_update_batch(
|
|
613
|
-
batch, field_groups, inheritance_chain, **django_kwargs
|
|
614
|
-
)
|
|
615
|
-
total_updated += batch_result
|
|
616
|
-
print(f"Batch {i//batch_size + 1} updated {batch_result} rows")
|
|
617
|
-
|
|
618
|
-
print(f"\n=== TOTAL UPDATED: {total_updated} ===")
|
|
619
|
-
return total_updated
|
|
620
|
-
|
|
621
|
-
def _process_mti_bulk_update_batch(self, batch, field_groups, inheritance_chain, **kwargs):
|
|
622
|
-
"""
|
|
623
|
-
Process a single batch of objects for MTI bulk update.
|
|
624
|
-
Updates each table in the inheritance chain for the batch.
|
|
625
|
-
"""
|
|
626
|
-
total_updated = 0
|
|
627
|
-
|
|
628
|
-
print(f"Processing batch with {len(batch)} objects")
|
|
629
|
-
print(f"Field groups: {field_groups}")
|
|
630
|
-
|
|
631
|
-
#
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
#
|
|
663
|
-
pks =
|
|
664
|
-
filter_field =
|
|
665
|
-
print(f"
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
print(
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1
|
+
from django.db import models, transaction
|
|
2
|
+
from django.db.models import AutoField
|
|
3
|
+
|
|
4
|
+
from django_bulk_hooks import engine
|
|
5
|
+
from django_bulk_hooks.constants import (
|
|
6
|
+
AFTER_CREATE,
|
|
7
|
+
AFTER_DELETE,
|
|
8
|
+
AFTER_UPDATE,
|
|
9
|
+
BEFORE_CREATE,
|
|
10
|
+
BEFORE_DELETE,
|
|
11
|
+
BEFORE_UPDATE,
|
|
12
|
+
VALIDATE_CREATE,
|
|
13
|
+
VALIDATE_DELETE,
|
|
14
|
+
VALIDATE_UPDATE,
|
|
15
|
+
)
|
|
16
|
+
from django_bulk_hooks.context import HookContext
|
|
17
|
+
from django.db.models import When, Value, Case
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HookQuerySet(models.QuerySet):
|
|
21
|
+
@transaction.atomic
|
|
22
|
+
def delete(self):
|
|
23
|
+
objs = list(self)
|
|
24
|
+
if not objs:
|
|
25
|
+
return 0
|
|
26
|
+
|
|
27
|
+
model_cls = self.model
|
|
28
|
+
ctx = HookContext(model_cls)
|
|
29
|
+
|
|
30
|
+
# Run validation hooks first
|
|
31
|
+
engine.run(model_cls, VALIDATE_DELETE, objs, ctx=ctx)
|
|
32
|
+
|
|
33
|
+
# Then run business logic hooks
|
|
34
|
+
engine.run(model_cls, BEFORE_DELETE, objs, ctx=ctx)
|
|
35
|
+
|
|
36
|
+
# Use Django's standard delete() method
|
|
37
|
+
result = super().delete()
|
|
38
|
+
|
|
39
|
+
# Run AFTER_DELETE hooks
|
|
40
|
+
engine.run(model_cls, AFTER_DELETE, objs, ctx=ctx)
|
|
41
|
+
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
@transaction.atomic
|
|
45
|
+
def update(self, **kwargs):
|
|
46
|
+
instances = list(self)
|
|
47
|
+
if not instances:
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
model_cls = self.model
|
|
51
|
+
pks = [obj.pk for obj in instances]
|
|
52
|
+
|
|
53
|
+
# Load originals for hook comparison and ensure they match the order of instances
|
|
54
|
+
# Use the base manager to avoid recursion
|
|
55
|
+
original_map = {
|
|
56
|
+
obj.pk: obj for obj in model_cls._base_manager.filter(pk__in=pks)
|
|
57
|
+
}
|
|
58
|
+
originals = [original_map.get(obj.pk) for obj in instances]
|
|
59
|
+
|
|
60
|
+
# Apply field updates to instances
|
|
61
|
+
for obj in instances:
|
|
62
|
+
for field, value in kwargs.items():
|
|
63
|
+
setattr(obj, field, value)
|
|
64
|
+
|
|
65
|
+
# Run BEFORE_UPDATE hooks
|
|
66
|
+
ctx = HookContext(model_cls)
|
|
67
|
+
engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
|
|
68
|
+
|
|
69
|
+
# Use Django's built-in update logic directly
|
|
70
|
+
# Call the base QuerySet implementation to avoid recursion
|
|
71
|
+
update_count = super().update(**kwargs)
|
|
72
|
+
|
|
73
|
+
# Run AFTER_UPDATE hooks
|
|
74
|
+
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
75
|
+
|
|
76
|
+
return update_count
|
|
77
|
+
|
|
78
|
+
@transaction.atomic
|
|
79
|
+
def bulk_create(
|
|
80
|
+
self,
|
|
81
|
+
objs,
|
|
82
|
+
batch_size=None,
|
|
83
|
+
ignore_conflicts=False,
|
|
84
|
+
update_conflicts=False,
|
|
85
|
+
update_fields=None,
|
|
86
|
+
unique_fields=None,
|
|
87
|
+
bypass_hooks=False,
|
|
88
|
+
bypass_validation=False,
|
|
89
|
+
):
|
|
90
|
+
"""
|
|
91
|
+
Insert each of the instances into the database. Behaves like Django's bulk_create,
|
|
92
|
+
but supports multi-table inheritance (MTI) models and hooks. All arguments are supported and
|
|
93
|
+
passed through to the correct logic. For MTI, only a subset of options may be supported.
|
|
94
|
+
"""
|
|
95
|
+
model_cls = self.model
|
|
96
|
+
|
|
97
|
+
# When you bulk insert you don't get the primary keys back (if it's an
|
|
98
|
+
# autoincrement, except if can_return_rows_from_bulk_insert=True), so
|
|
99
|
+
# you can't insert into the child tables which references this. There
|
|
100
|
+
# are two workarounds:
|
|
101
|
+
# 1) This could be implemented if you didn't have an autoincrement pk
|
|
102
|
+
# 2) You could do it by doing O(n) normal inserts into the parent
|
|
103
|
+
# tables to get the primary keys back and then doing a single bulk
|
|
104
|
+
# insert into the childmost table.
|
|
105
|
+
# We currently set the primary keys on the objects when using
|
|
106
|
+
# PostgreSQL via the RETURNING ID clause. It should be possible for
|
|
107
|
+
# Oracle as well, but the semantics for extracting the primary keys is
|
|
108
|
+
# trickier so it's not done yet.
|
|
109
|
+
if batch_size is not None and batch_size <= 0:
|
|
110
|
+
raise ValueError("Batch size must be a positive integer.")
|
|
111
|
+
|
|
112
|
+
if not objs:
|
|
113
|
+
return objs
|
|
114
|
+
|
|
115
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
116
|
+
raise TypeError(
|
|
117
|
+
f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Check for MTI - if we detect multi-table inheritance, we need special handling
|
|
121
|
+
# This follows Django's approach: check that the parents share the same concrete model
|
|
122
|
+
# with our model to detect the inheritance pattern ConcreteGrandParent ->
|
|
123
|
+
# MultiTableParent -> ProxyChild. Simply checking self.model._meta.proxy would not
|
|
124
|
+
# identify that case as involving multiple tables.
|
|
125
|
+
is_mti = False
|
|
126
|
+
for parent in model_cls._meta.all_parents:
|
|
127
|
+
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
128
|
+
is_mti = True
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
# Fire hooks before DB ops
|
|
132
|
+
if not bypass_hooks:
|
|
133
|
+
ctx = HookContext(model_cls)
|
|
134
|
+
if not bypass_validation:
|
|
135
|
+
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
136
|
+
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
137
|
+
|
|
138
|
+
# For MTI models, we need to handle them specially
|
|
139
|
+
if is_mti:
|
|
140
|
+
# Use our MTI-specific logic
|
|
141
|
+
# Filter out custom parameters that Django's bulk_create doesn't accept
|
|
142
|
+
mti_kwargs = {
|
|
143
|
+
"batch_size": batch_size,
|
|
144
|
+
"ignore_conflicts": ignore_conflicts,
|
|
145
|
+
"update_conflicts": update_conflicts,
|
|
146
|
+
"update_fields": update_fields,
|
|
147
|
+
"unique_fields": unique_fields,
|
|
148
|
+
}
|
|
149
|
+
# Remove custom hook kwargs if present in self.bulk_create signature
|
|
150
|
+
result = self._mti_bulk_create(
|
|
151
|
+
objs,
|
|
152
|
+
**mti_kwargs,
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
# For single-table models, use Django's built-in bulk_create
|
|
156
|
+
# but we need to call it on the base manager to avoid recursion
|
|
157
|
+
# Filter out custom parameters that Django's bulk_create doesn't accept
|
|
158
|
+
|
|
159
|
+
result = super().bulk_create(
|
|
160
|
+
objs,
|
|
161
|
+
batch_size=batch_size,
|
|
162
|
+
ignore_conflicts=ignore_conflicts,
|
|
163
|
+
update_conflicts=update_conflicts,
|
|
164
|
+
update_fields=update_fields,
|
|
165
|
+
unique_fields=unique_fields,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Fire AFTER_CREATE hooks
|
|
169
|
+
if not bypass_hooks:
|
|
170
|
+
engine.run(model_cls, AFTER_CREATE, objs, ctx=ctx)
|
|
171
|
+
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
@transaction.atomic
|
|
175
|
+
def bulk_update(
|
|
176
|
+
self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
177
|
+
):
|
|
178
|
+
"""
|
|
179
|
+
Bulk update objects in the database with MTI support.
|
|
180
|
+
"""
|
|
181
|
+
print(f"\n=== BULK UPDATE DEBUG ===")
|
|
182
|
+
print(f"Model: {self.model.__name__}")
|
|
183
|
+
print(f"Number of objects: {len(objs)}")
|
|
184
|
+
print(f"Fields: {fields}")
|
|
185
|
+
print(f"Bypass hooks: {bypass_hooks}")
|
|
186
|
+
print(f"Bypass validation: {bypass_validation}")
|
|
187
|
+
|
|
188
|
+
model_cls = self.model
|
|
189
|
+
|
|
190
|
+
if not objs:
|
|
191
|
+
print("No objects to update")
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
195
|
+
raise TypeError(
|
|
196
|
+
f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# Check for MTI
|
|
200
|
+
is_mti = False
|
|
201
|
+
for parent in model_cls._meta.all_parents:
|
|
202
|
+
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
203
|
+
is_mti = True
|
|
204
|
+
break
|
|
205
|
+
|
|
206
|
+
print(f"Is MTI: {is_mti}")
|
|
207
|
+
print(f"Model concrete model: {model_cls._meta.concrete_model.__name__}")
|
|
208
|
+
for parent in model_cls._meta.all_parents:
|
|
209
|
+
print(f" Parent {parent.__name__}: concrete_model = {parent._meta.concrete_model.__name__}")
|
|
210
|
+
|
|
211
|
+
if not bypass_hooks:
|
|
212
|
+
# Load originals for hook comparison
|
|
213
|
+
original_map = {
|
|
214
|
+
obj.pk: obj
|
|
215
|
+
for obj in model_cls._base_manager.filter(
|
|
216
|
+
pk__in=[obj.pk for obj in objs]
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
originals = [original_map.get(obj.pk) for obj in objs]
|
|
220
|
+
|
|
221
|
+
ctx = HookContext(model_cls)
|
|
222
|
+
|
|
223
|
+
# Run validation hooks first
|
|
224
|
+
if not bypass_validation:
|
|
225
|
+
engine.run(model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx)
|
|
226
|
+
|
|
227
|
+
# Then run business logic hooks
|
|
228
|
+
engine.run(model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx)
|
|
229
|
+
|
|
230
|
+
# Detect modified fields during hooks
|
|
231
|
+
modified_fields = self._detect_modified_fields(objs, originals)
|
|
232
|
+
if modified_fields:
|
|
233
|
+
fields_set = set(fields)
|
|
234
|
+
fields_set.update(modified_fields)
|
|
235
|
+
fields = list(fields_set)
|
|
236
|
+
print(f"Modified fields detected: {modified_fields}")
|
|
237
|
+
|
|
238
|
+
# Handle MTI models differently
|
|
239
|
+
if is_mti:
|
|
240
|
+
print("Using MTI bulk update logic")
|
|
241
|
+
result = self._mti_bulk_update(objs, fields, **kwargs)
|
|
242
|
+
else:
|
|
243
|
+
print("Using standard Django bulk_update")
|
|
244
|
+
# For single-table models, use Django's built-in bulk_update
|
|
245
|
+
django_kwargs = {
|
|
246
|
+
k: v
|
|
247
|
+
for k, v in kwargs.items()
|
|
248
|
+
if k not in ["bypass_hooks", "bypass_validation"]
|
|
249
|
+
}
|
|
250
|
+
result = super().bulk_update(objs, fields, **django_kwargs)
|
|
251
|
+
|
|
252
|
+
if not bypass_hooks:
|
|
253
|
+
engine.run(model_cls, AFTER_UPDATE, objs, originals, ctx=ctx)
|
|
254
|
+
|
|
255
|
+
print(f"Bulk update result: {result}")
|
|
256
|
+
return result
|
|
257
|
+
|
|
258
|
+
def _detect_modified_fields(self, new_instances, original_instances):
|
|
259
|
+
"""
|
|
260
|
+
Detect fields that were modified during BEFORE_UPDATE hooks by comparing
|
|
261
|
+
new instances with their original values.
|
|
262
|
+
"""
|
|
263
|
+
if not original_instances:
|
|
264
|
+
return set()
|
|
265
|
+
|
|
266
|
+
modified_fields = set()
|
|
267
|
+
|
|
268
|
+
# Since original_instances is now ordered to match new_instances, we can zip them directly
|
|
269
|
+
for new_instance, original in zip(new_instances, original_instances):
|
|
270
|
+
if new_instance.pk is None or original is None:
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
# Compare all fields to detect changes
|
|
274
|
+
for field in new_instance._meta.fields:
|
|
275
|
+
if field.name == "id":
|
|
276
|
+
continue
|
|
277
|
+
|
|
278
|
+
new_value = getattr(new_instance, field.name)
|
|
279
|
+
original_value = getattr(original, field.name)
|
|
280
|
+
|
|
281
|
+
# Handle different field types appropriately
|
|
282
|
+
if field.is_relation:
|
|
283
|
+
# For foreign keys, compare the pk values
|
|
284
|
+
new_pk = new_value.pk if new_value else None
|
|
285
|
+
original_pk = original_value.pk if original_value else None
|
|
286
|
+
if new_pk != original_pk:
|
|
287
|
+
modified_fields.add(field.name)
|
|
288
|
+
else:
|
|
289
|
+
# For regular fields, use direct comparison
|
|
290
|
+
if new_value != original_value:
|
|
291
|
+
modified_fields.add(field.name)
|
|
292
|
+
|
|
293
|
+
return modified_fields
|
|
294
|
+
|
|
295
|
+
def _get_inheritance_chain(self):
|
|
296
|
+
"""
|
|
297
|
+
Get the complete inheritance chain from root parent to current model.
|
|
298
|
+
Returns list of model classes in order: [RootParent, Parent, Child]
|
|
299
|
+
"""
|
|
300
|
+
print(f"\n=== GET INHERITANCE CHAIN DEBUG ===")
|
|
301
|
+
print(f"Current model: {self.model.__name__}")
|
|
302
|
+
|
|
303
|
+
chain = []
|
|
304
|
+
current_model = self.model
|
|
305
|
+
while current_model:
|
|
306
|
+
print(f"Processing model: {current_model.__name__}")
|
|
307
|
+
if not current_model._meta.proxy:
|
|
308
|
+
chain.append(current_model)
|
|
309
|
+
print(f" Added to chain: {current_model.__name__}")
|
|
310
|
+
else:
|
|
311
|
+
print(f" Skipped proxy model: {current_model.__name__}")
|
|
312
|
+
|
|
313
|
+
parents = [
|
|
314
|
+
parent
|
|
315
|
+
for parent in current_model._meta.parents.keys()
|
|
316
|
+
if not parent._meta.proxy
|
|
317
|
+
]
|
|
318
|
+
print(f" Parents: {[p.__name__ for p in parents]}")
|
|
319
|
+
current_model = parents[0] if parents else None
|
|
320
|
+
|
|
321
|
+
chain.reverse()
|
|
322
|
+
print(f"Final inheritance chain: {[m.__name__ for m in chain]}")
|
|
323
|
+
return chain
|
|
324
|
+
|
|
325
|
+
def _mti_bulk_create(self, objs, inheritance_chain=None, **kwargs):
|
|
326
|
+
"""
|
|
327
|
+
Implements Django's suggested workaround #2 for MTI bulk_create:
|
|
328
|
+
O(n) normal inserts into parent tables to get primary keys back,
|
|
329
|
+
then single bulk insert into childmost table.
|
|
330
|
+
Sets auto_now_add/auto_now fields for each model in the chain.
|
|
331
|
+
"""
|
|
332
|
+
# Remove custom hook kwargs before passing to Django internals
|
|
333
|
+
django_kwargs = {
|
|
334
|
+
k: v
|
|
335
|
+
for k, v in kwargs.items()
|
|
336
|
+
if k not in ["bypass_hooks", "bypass_validation"]
|
|
337
|
+
}
|
|
338
|
+
if inheritance_chain is None:
|
|
339
|
+
inheritance_chain = self._get_inheritance_chain()
|
|
340
|
+
|
|
341
|
+
# Safety check to prevent infinite recursion
|
|
342
|
+
if len(inheritance_chain) > 10: # Arbitrary limit to prevent infinite loops
|
|
343
|
+
raise ValueError(
|
|
344
|
+
"Inheritance chain too deep - possible infinite recursion detected"
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
batch_size = django_kwargs.get("batch_size") or len(objs)
|
|
348
|
+
created_objects = []
|
|
349
|
+
with transaction.atomic(using=self.db, savepoint=False):
|
|
350
|
+
for i in range(0, len(objs), batch_size):
|
|
351
|
+
batch = objs[i : i + batch_size]
|
|
352
|
+
batch_result = self._process_mti_bulk_create_batch(
|
|
353
|
+
batch, inheritance_chain, **django_kwargs
|
|
354
|
+
)
|
|
355
|
+
created_objects.extend(batch_result)
|
|
356
|
+
return created_objects
|
|
357
|
+
|
|
358
|
+
def _process_mti_bulk_create_batch(self, batch, inheritance_chain, **kwargs):
|
|
359
|
+
"""
|
|
360
|
+
Process a single batch of objects through the inheritance chain.
|
|
361
|
+
Implements Django's suggested workaround #2: O(n) normal inserts into parent
|
|
362
|
+
tables to get primary keys back, then single bulk insert into childmost table.
|
|
363
|
+
"""
|
|
364
|
+
# For MTI, we need to save parent objects first to get PKs
|
|
365
|
+
# Then we can use Django's bulk_create for the child objects
|
|
366
|
+
parent_objects_map = {}
|
|
367
|
+
|
|
368
|
+
# Step 1: Do O(n) normal inserts into parent tables to get primary keys back
|
|
369
|
+
# Get bypass_hooks from kwargs
|
|
370
|
+
bypass_hooks = kwargs.get("bypass_hooks", False)
|
|
371
|
+
bypass_validation = kwargs.get("bypass_validation", False)
|
|
372
|
+
|
|
373
|
+
for obj in batch:
|
|
374
|
+
parent_instances = {}
|
|
375
|
+
current_parent = None
|
|
376
|
+
for model_class in inheritance_chain[:-1]:
|
|
377
|
+
parent_obj = self._create_parent_instance(
|
|
378
|
+
obj, model_class, current_parent
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Fire parent hooks if not bypassed
|
|
382
|
+
if not bypass_hooks:
|
|
383
|
+
ctx = HookContext(model_class)
|
|
384
|
+
if not bypass_validation:
|
|
385
|
+
engine.run(model_class, VALIDATE_CREATE, [parent_obj], ctx=ctx)
|
|
386
|
+
engine.run(model_class, BEFORE_CREATE, [parent_obj], ctx=ctx)
|
|
387
|
+
|
|
388
|
+
# Use Django's base manager to create the object and get PKs back
|
|
389
|
+
# This bypasses hooks and the MTI exception
|
|
390
|
+
field_values = {
|
|
391
|
+
field.name: getattr(parent_obj, field.name)
|
|
392
|
+
for field in model_class._meta.local_fields
|
|
393
|
+
if hasattr(parent_obj, field.name)
|
|
394
|
+
and getattr(parent_obj, field.name) is not None
|
|
395
|
+
}
|
|
396
|
+
created_obj = model_class._base_manager.using(self.db).create(
|
|
397
|
+
**field_values
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
# Update the parent_obj with the created object's PK
|
|
401
|
+
parent_obj.pk = created_obj.pk
|
|
402
|
+
parent_obj._state.adding = False
|
|
403
|
+
parent_obj._state.db = self.db
|
|
404
|
+
|
|
405
|
+
# Fire AFTER_CREATE hooks for parent
|
|
406
|
+
if not bypass_hooks:
|
|
407
|
+
engine.run(model_class, AFTER_CREATE, [parent_obj], ctx=ctx)
|
|
408
|
+
|
|
409
|
+
parent_instances[model_class] = parent_obj
|
|
410
|
+
current_parent = parent_obj
|
|
411
|
+
parent_objects_map[id(obj)] = parent_instances
|
|
412
|
+
|
|
413
|
+
# Step 2: Create all child objects and do single bulk insert into childmost table
|
|
414
|
+
child_model = inheritance_chain[-1]
|
|
415
|
+
all_child_objects = []
|
|
416
|
+
for obj in batch:
|
|
417
|
+
child_obj = self._create_child_instance(
|
|
418
|
+
obj, child_model, parent_objects_map.get(id(obj), {})
|
|
419
|
+
)
|
|
420
|
+
all_child_objects.append(child_obj)
|
|
421
|
+
|
|
422
|
+
# Step 2.5: Use Django's internal bulk_create infrastructure
|
|
423
|
+
if all_child_objects:
|
|
424
|
+
# Get the base manager's queryset
|
|
425
|
+
base_qs = child_model._base_manager.using(self.db)
|
|
426
|
+
|
|
427
|
+
# Use Django's exact approach: call _prepare_for_bulk_create then partition
|
|
428
|
+
base_qs._prepare_for_bulk_create(all_child_objects)
|
|
429
|
+
|
|
430
|
+
# Implement our own partition since itertools.partition might not be available
|
|
431
|
+
objs_without_pk, objs_with_pk = [], []
|
|
432
|
+
for obj in all_child_objects:
|
|
433
|
+
if obj._is_pk_set():
|
|
434
|
+
objs_with_pk.append(obj)
|
|
435
|
+
else:
|
|
436
|
+
objs_without_pk.append(obj)
|
|
437
|
+
|
|
438
|
+
# Use Django's internal _batched_insert method
|
|
439
|
+
opts = child_model._meta
|
|
440
|
+
# For child models in MTI, we need to include the foreign key to the parent
|
|
441
|
+
# but exclude the primary key since it's inherited
|
|
442
|
+
|
|
443
|
+
# Include all local fields except generated ones
|
|
444
|
+
# We need to include the foreign key to the parent (business_ptr)
|
|
445
|
+
fields = [f for f in opts.local_fields if not f.generated]
|
|
446
|
+
|
|
447
|
+
with transaction.atomic(using=self.db, savepoint=False):
|
|
448
|
+
if objs_with_pk:
|
|
449
|
+
returned_columns = base_qs._batched_insert(
|
|
450
|
+
objs_with_pk,
|
|
451
|
+
fields,
|
|
452
|
+
batch_size=len(objs_with_pk), # Use actual batch size
|
|
453
|
+
)
|
|
454
|
+
for obj_with_pk, results in zip(objs_with_pk, returned_columns):
|
|
455
|
+
for result, field in zip(results, opts.db_returning_fields):
|
|
456
|
+
if field != opts.pk:
|
|
457
|
+
setattr(obj_with_pk, field.attname, result)
|
|
458
|
+
for obj_with_pk in objs_with_pk:
|
|
459
|
+
obj_with_pk._state.adding = False
|
|
460
|
+
obj_with_pk._state.db = self.db
|
|
461
|
+
|
|
462
|
+
if objs_without_pk:
|
|
463
|
+
# For objects without PK, we still need to exclude primary key fields
|
|
464
|
+
fields = [
|
|
465
|
+
f
|
|
466
|
+
for f in fields
|
|
467
|
+
if not isinstance(f, AutoField) and not f.primary_key
|
|
468
|
+
]
|
|
469
|
+
returned_columns = base_qs._batched_insert(
|
|
470
|
+
objs_without_pk,
|
|
471
|
+
fields,
|
|
472
|
+
batch_size=len(objs_without_pk), # Use actual batch size
|
|
473
|
+
)
|
|
474
|
+
for obj_without_pk, results in zip(
|
|
475
|
+
objs_without_pk, returned_columns
|
|
476
|
+
):
|
|
477
|
+
for result, field in zip(results, opts.db_returning_fields):
|
|
478
|
+
setattr(obj_without_pk, field.attname, result)
|
|
479
|
+
obj_without_pk._state.adding = False
|
|
480
|
+
obj_without_pk._state.db = self.db
|
|
481
|
+
|
|
482
|
+
# Step 3: Update original objects with generated PKs and state
|
|
483
|
+
pk_field_name = child_model._meta.pk.name
|
|
484
|
+
for orig_obj, child_obj in zip(batch, all_child_objects):
|
|
485
|
+
child_pk = getattr(child_obj, pk_field_name)
|
|
486
|
+
setattr(orig_obj, pk_field_name, child_pk)
|
|
487
|
+
orig_obj._state.adding = False
|
|
488
|
+
orig_obj._state.db = self.db
|
|
489
|
+
|
|
490
|
+
return batch
|
|
491
|
+
|
|
492
|
+
def _create_parent_instance(self, source_obj, parent_model, current_parent):
|
|
493
|
+
parent_obj = parent_model()
|
|
494
|
+
for field in parent_model._meta.local_fields:
|
|
495
|
+
# Only copy if the field exists on the source and is not None
|
|
496
|
+
if hasattr(source_obj, field.name):
|
|
497
|
+
value = getattr(source_obj, field.name, None)
|
|
498
|
+
if value is not None:
|
|
499
|
+
setattr(parent_obj, field.name, value)
|
|
500
|
+
if current_parent is not None:
|
|
501
|
+
for field in parent_model._meta.local_fields:
|
|
502
|
+
if (
|
|
503
|
+
hasattr(field, "remote_field")
|
|
504
|
+
and field.remote_field
|
|
505
|
+
and field.remote_field.model == current_parent.__class__
|
|
506
|
+
):
|
|
507
|
+
setattr(parent_obj, field.name, current_parent)
|
|
508
|
+
break
|
|
509
|
+
|
|
510
|
+
# Handle auto_now_add and auto_now fields like Django does
|
|
511
|
+
for field in parent_model._meta.local_fields:
|
|
512
|
+
if hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
513
|
+
# Ensure auto_now_add fields are properly set
|
|
514
|
+
if getattr(parent_obj, field.name) is None:
|
|
515
|
+
field.pre_save(parent_obj, add=True)
|
|
516
|
+
# Explicitly set the value to ensure it's not None
|
|
517
|
+
setattr(parent_obj, field.name, field.value_from_object(parent_obj))
|
|
518
|
+
elif hasattr(field, "auto_now") and field.auto_now:
|
|
519
|
+
field.pre_save(parent_obj, add=True)
|
|
520
|
+
|
|
521
|
+
return parent_obj
|
|
522
|
+
|
|
523
|
+
def _create_child_instance(self, source_obj, child_model, parent_instances):
|
|
524
|
+
child_obj = child_model()
|
|
525
|
+
# Only copy fields that exist in the child model's local fields
|
|
526
|
+
for field in child_model._meta.local_fields:
|
|
527
|
+
if isinstance(field, AutoField):
|
|
528
|
+
continue
|
|
529
|
+
if hasattr(source_obj, field.name):
|
|
530
|
+
value = getattr(source_obj, field.name, None)
|
|
531
|
+
if value is not None:
|
|
532
|
+
setattr(child_obj, field.name, value)
|
|
533
|
+
|
|
534
|
+
# Set parent links for MTI
|
|
535
|
+
for parent_model, parent_instance in parent_instances.items():
|
|
536
|
+
parent_link = child_model._meta.get_ancestor_link(parent_model)
|
|
537
|
+
if parent_link:
|
|
538
|
+
# Set both the foreign key value (the ID) and the object reference
|
|
539
|
+
# This follows Django's pattern in _set_pk_val
|
|
540
|
+
setattr(
|
|
541
|
+
child_obj, parent_link.attname, parent_instance.pk
|
|
542
|
+
) # Set the foreign key value
|
|
543
|
+
setattr(
|
|
544
|
+
child_obj, parent_link.name, parent_instance
|
|
545
|
+
) # Set the object reference
|
|
546
|
+
|
|
547
|
+
# Handle auto_now_add and auto_now fields like Django does
|
|
548
|
+
for field in child_model._meta.local_fields:
|
|
549
|
+
if hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
550
|
+
# Ensure auto_now_add fields are properly set
|
|
551
|
+
if getattr(child_obj, field.name) is None:
|
|
552
|
+
field.pre_save(child_obj, add=True)
|
|
553
|
+
# Explicitly set the value to ensure it's not None
|
|
554
|
+
setattr(child_obj, field.name, field.value_from_object(child_obj))
|
|
555
|
+
elif hasattr(field, "auto_now") and field.auto_now:
|
|
556
|
+
field.pre_save(child_obj, add=True)
|
|
557
|
+
|
|
558
|
+
return child_obj
|
|
559
|
+
|
|
560
|
+
def _mti_bulk_update(self, objs, fields, **kwargs):
|
|
561
|
+
"""
|
|
562
|
+
Custom bulk update implementation for MTI models.
|
|
563
|
+
Updates each table in the inheritance chain efficiently using Django's batch_size.
|
|
564
|
+
"""
|
|
565
|
+
print(f"\n=== MTI BULK UPDATE DEBUG ===")
|
|
566
|
+
print(f"Model: {self.model.__name__}")
|
|
567
|
+
print(f"Number of objects: {len(objs)}")
|
|
568
|
+
print(f"Fields to update: {fields}")
|
|
569
|
+
|
|
570
|
+
model_cls = self.model
|
|
571
|
+
inheritance_chain = self._get_inheritance_chain()
|
|
572
|
+
print(f"Inheritance chain: {[m.__name__ for m in inheritance_chain]}")
|
|
573
|
+
|
|
574
|
+
# Remove custom hook kwargs before passing to Django internals
|
|
575
|
+
django_kwargs = {
|
|
576
|
+
k: v
|
|
577
|
+
for k, v in kwargs.items()
|
|
578
|
+
if k not in ["bypass_hooks", "bypass_validation"]
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
# Safety check to prevent infinite recursion
|
|
582
|
+
if len(inheritance_chain) > 10: # Arbitrary limit to prevent infinite loops
|
|
583
|
+
raise ValueError(
|
|
584
|
+
"Inheritance chain too deep - possible infinite recursion detected"
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# Group fields by model in the inheritance chain
|
|
588
|
+
field_groups = {}
|
|
589
|
+
for field_name in fields:
|
|
590
|
+
field = model_cls._meta.get_field(field_name)
|
|
591
|
+
# Find which model in the inheritance chain this field belongs to
|
|
592
|
+
for model in inheritance_chain:
|
|
593
|
+
if field in model._meta.local_fields:
|
|
594
|
+
if model not in field_groups:
|
|
595
|
+
field_groups[model] = []
|
|
596
|
+
field_groups[model].append(field_name)
|
|
597
|
+
print(f"Field '{field_name}' belongs to model '{model.__name__}'")
|
|
598
|
+
break
|
|
599
|
+
|
|
600
|
+
print(f"Field groups: {field_groups}")
|
|
601
|
+
|
|
602
|
+
# Process in batches
|
|
603
|
+
batch_size = django_kwargs.get("batch_size") or len(objs)
|
|
604
|
+
total_updated = 0
|
|
605
|
+
|
|
606
|
+
print(f"Processing in batches of size: {batch_size}")
|
|
607
|
+
|
|
608
|
+
with transaction.atomic(using=self.db, savepoint=False):
|
|
609
|
+
for i in range(0, len(objs), batch_size):
|
|
610
|
+
batch = objs[i : i + batch_size]
|
|
611
|
+
print(f"\n--- Processing batch {i//batch_size + 1} ({len(batch)} objects) ---")
|
|
612
|
+
batch_result = self._process_mti_bulk_update_batch(
|
|
613
|
+
batch, field_groups, inheritance_chain, **django_kwargs
|
|
614
|
+
)
|
|
615
|
+
total_updated += batch_result
|
|
616
|
+
print(f"Batch {i//batch_size + 1} updated {batch_result} rows")
|
|
617
|
+
|
|
618
|
+
print(f"\n=== TOTAL UPDATED: {total_updated} ===")
|
|
619
|
+
return total_updated
|
|
620
|
+
|
|
621
|
+
def _process_mti_bulk_update_batch(self, batch, field_groups, inheritance_chain, **kwargs):
|
|
622
|
+
"""
|
|
623
|
+
Process a single batch of objects for MTI bulk update.
|
|
624
|
+
Updates each table in the inheritance chain for the batch.
|
|
625
|
+
"""
|
|
626
|
+
total_updated = 0
|
|
627
|
+
|
|
628
|
+
print(f"Processing batch with {len(batch)} objects")
|
|
629
|
+
print(f"Field groups: {field_groups}")
|
|
630
|
+
|
|
631
|
+
# For MTI, we need to handle parent links correctly
|
|
632
|
+
# The root model (first in chain) has its own PK
|
|
633
|
+
# Child models use the parent link to reference the root PK
|
|
634
|
+
root_model = inheritance_chain[0]
|
|
635
|
+
|
|
636
|
+
# Get the primary keys from the objects
|
|
637
|
+
# If objects have pk set but are not loaded from DB, use those PKs
|
|
638
|
+
root_pks = []
|
|
639
|
+
for obj in batch:
|
|
640
|
+
if obj.pk is not None:
|
|
641
|
+
root_pks.append(obj.pk)
|
|
642
|
+
else:
|
|
643
|
+
print(f"WARNING: Object {obj} has no primary key")
|
|
644
|
+
continue
|
|
645
|
+
|
|
646
|
+
print(f"Root PKs to update: {root_pks}")
|
|
647
|
+
|
|
648
|
+
if not root_pks:
|
|
649
|
+
print("No valid primary keys found, skipping update")
|
|
650
|
+
return 0
|
|
651
|
+
|
|
652
|
+
# Update each table in the inheritance chain
|
|
653
|
+
for model, model_fields in field_groups.items():
|
|
654
|
+
print(f"\n--- Updating model: {model.__name__} ---")
|
|
655
|
+
print(f"Fields to update: {model_fields}")
|
|
656
|
+
|
|
657
|
+
if not model_fields:
|
|
658
|
+
print("No fields to update, skipping")
|
|
659
|
+
continue
|
|
660
|
+
|
|
661
|
+
if model == inheritance_chain[0]:
|
|
662
|
+
# Root model - use primary keys directly
|
|
663
|
+
pks = root_pks
|
|
664
|
+
filter_field = 'pk'
|
|
665
|
+
print(f"Root model - using PKs: {pks}")
|
|
666
|
+
else:
|
|
667
|
+
# Child model - use parent link field
|
|
668
|
+
parent_link = None
|
|
669
|
+
for parent_model in inheritance_chain:
|
|
670
|
+
if parent_model in model._meta.parents:
|
|
671
|
+
parent_link = model._meta.parents[parent_model]
|
|
672
|
+
break
|
|
673
|
+
|
|
674
|
+
if parent_link is None:
|
|
675
|
+
print(f"No parent link found for {model.__name__}, skipping")
|
|
676
|
+
continue
|
|
677
|
+
|
|
678
|
+
print(f"Parent link field: {parent_link.name} ({parent_link.attname})")
|
|
679
|
+
|
|
680
|
+
# For child models, the parent link values should be the same as root PKs
|
|
681
|
+
pks = root_pks
|
|
682
|
+
filter_field = parent_link.attname
|
|
683
|
+
print(f"Child model - using parent link values: {pks}")
|
|
684
|
+
|
|
685
|
+
if pks:
|
|
686
|
+
base_qs = model._base_manager.using(self.db)
|
|
687
|
+
print(f"Filter field: {filter_field}")
|
|
688
|
+
print(f"PKs to filter by: {pks}")
|
|
689
|
+
|
|
690
|
+
# Check if records exist
|
|
691
|
+
existing_count = base_qs.filter(**{f"{filter_field}__in": pks}).count()
|
|
692
|
+
print(f"Existing records with these PKs: {existing_count}")
|
|
693
|
+
|
|
694
|
+
if existing_count == 0:
|
|
695
|
+
print("WARNING: No existing records found with these PKs!")
|
|
696
|
+
continue
|
|
697
|
+
|
|
698
|
+
# Build CASE statements for each field to perform a single bulk update
|
|
699
|
+
case_statements = {}
|
|
700
|
+
for field_name in model_fields:
|
|
701
|
+
field = model._meta.get_field(field_name)
|
|
702
|
+
when_statements = []
|
|
703
|
+
|
|
704
|
+
print(f"Building CASE statement for field: {field_name}")
|
|
705
|
+
for pk, obj in zip(pks, batch):
|
|
706
|
+
if obj.pk is None:
|
|
707
|
+
continue
|
|
708
|
+
value = getattr(obj, field_name)
|
|
709
|
+
print(f" PK {pk}: {field_name} = {value}")
|
|
710
|
+
when_statements.append(When(**{filter_field: pk}, then=Value(value, output_field=field)))
|
|
711
|
+
|
|
712
|
+
case_statements[field_name] = Case(*when_statements, output_field=field)
|
|
713
|
+
|
|
714
|
+
print(f"Case statements built: {list(case_statements.keys())}")
|
|
715
|
+
|
|
716
|
+
# Execute a single bulk update for all objects in this model
|
|
717
|
+
try:
|
|
718
|
+
updated_count = base_qs.filter(**{f"{filter_field}__in": pks}).update(**case_statements)
|
|
719
|
+
print(f"UPDATE QUERY EXECUTED - Updated {updated_count} rows")
|
|
720
|
+
total_updated += updated_count
|
|
721
|
+
except Exception as e:
|
|
722
|
+
print(f"ERROR during update: {e}")
|
|
723
|
+
import traceback
|
|
724
|
+
traceback.print_exc()
|
|
725
|
+
else:
|
|
726
|
+
print("No PKs found, skipping update")
|
|
727
|
+
|
|
728
|
+
print(f"Batch total updated: {total_updated}")
|
|
729
|
+
return total_updated
|