django-bulk-hooks 0.1.231__py3-none-any.whl → 0.1.232__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/__init__.py +1 -12
- django_bulk_hooks/conditions.py +30 -33
- django_bulk_hooks/context.py +15 -43
- django_bulk_hooks/decorators.py +8 -111
- django_bulk_hooks/engine.py +41 -127
- django_bulk_hooks/enums.py +13 -10
- django_bulk_hooks/handler.py +73 -40
- django_bulk_hooks/manager.py +101 -123
- django_bulk_hooks/models.py +15 -51
- django_bulk_hooks/priority.py +6 -6
- django_bulk_hooks/queryset.py +166 -308
- django_bulk_hooks/registry.py +24 -191
- {django_bulk_hooks-0.1.231.dist-info → django_bulk_hooks-0.1.232.dist-info}/METADATA +14 -30
- django_bulk_hooks-0.1.232.dist-info/RECORD +17 -0
- django_bulk_hooks-0.1.231.dist-info/RECORD +0 -17
- {django_bulk_hooks-0.1.231.dist-info → django_bulk_hooks-0.1.232.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.1.231.dist-info → django_bulk_hooks-0.1.232.dist-info}/WHEEL +0 -0
django_bulk_hooks/queryset.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from django.db import models, transaction
|
|
3
|
+
from django.db.models import AutoField, Case, Field, Value, When
|
|
2
4
|
|
|
3
|
-
from django.db import models, transaction, connections
|
|
4
|
-
from django.db.models import AutoField, Case, Value, When
|
|
5
5
|
from django_bulk_hooks import engine
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
6
8
|
from django_bulk_hooks.constants import (
|
|
7
9
|
AFTER_CREATE,
|
|
8
10
|
AFTER_DELETE,
|
|
@@ -16,8 +18,6 @@ from django_bulk_hooks.constants import (
|
|
|
16
18
|
)
|
|
17
19
|
from django_bulk_hooks.context import HookContext
|
|
18
20
|
|
|
19
|
-
logger = logging.getLogger(__name__)
|
|
20
|
-
|
|
21
21
|
|
|
22
22
|
class HookQuerySetMixin:
|
|
23
23
|
"""
|
|
@@ -26,24 +26,12 @@ class HookQuerySetMixin:
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
@transaction.atomic
|
|
29
|
-
def delete(self)
|
|
30
|
-
"""
|
|
31
|
-
Delete objects from the database with complete hook support.
|
|
32
|
-
|
|
33
|
-
This method runs the complete hook cycle:
|
|
34
|
-
VALIDATE_DELETE → BEFORE_DELETE → DB delete → AFTER_DELETE
|
|
35
|
-
"""
|
|
29
|
+
def delete(self):
|
|
36
30
|
objs = list(self)
|
|
37
31
|
if not objs:
|
|
38
32
|
return 0
|
|
39
33
|
|
|
40
34
|
model_cls = self.model
|
|
41
|
-
|
|
42
|
-
# Validate that all objects have primary keys
|
|
43
|
-
for obj in objs:
|
|
44
|
-
if obj.pk is None:
|
|
45
|
-
raise ValueError("Cannot delete objects without primary keys")
|
|
46
|
-
|
|
47
35
|
ctx = HookContext(model_cls)
|
|
48
36
|
|
|
49
37
|
# Run validation hooks first
|
|
@@ -61,19 +49,7 @@ class HookQuerySetMixin:
|
|
|
61
49
|
return result
|
|
62
50
|
|
|
63
51
|
@transaction.atomic
|
|
64
|
-
def update(self, **kwargs)
|
|
65
|
-
"""
|
|
66
|
-
Update objects with field values and run complete hook cycle.
|
|
67
|
-
|
|
68
|
-
This method runs the complete hook cycle for all updates:
|
|
69
|
-
VALIDATE_UPDATE → BEFORE_UPDATE → DB update → AFTER_UPDATE
|
|
70
|
-
|
|
71
|
-
Supports both simple field updates and complex expressions (Subquery, Case, etc.).
|
|
72
|
-
"""
|
|
73
|
-
# Extract custom parameters
|
|
74
|
-
bypass_hooks = kwargs.pop('bypass_hooks', False)
|
|
75
|
-
bypass_validation = kwargs.pop('bypass_validation', False)
|
|
76
|
-
|
|
52
|
+
def update(self, **kwargs):
|
|
77
53
|
instances = list(self)
|
|
78
54
|
if not instances:
|
|
79
55
|
return 0
|
|
@@ -81,105 +57,73 @@ class HookQuerySetMixin:
|
|
|
81
57
|
model_cls = self.model
|
|
82
58
|
pks = [obj.pk for obj in instances]
|
|
83
59
|
|
|
84
|
-
# Load originals for hook comparison
|
|
60
|
+
# Load originals for hook comparison and ensure they match the order of instances
|
|
61
|
+
# Use the base manager to avoid recursion
|
|
85
62
|
original_map = {
|
|
86
63
|
obj.pk: obj for obj in model_cls._base_manager.filter(pk__in=pks)
|
|
87
64
|
}
|
|
88
65
|
originals = [original_map.get(obj.pk) for obj in instances]
|
|
89
66
|
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
(hasattr(value, "query") and hasattr(value.query, "model"))
|
|
96
|
-
or (
|
|
97
|
-
hasattr(value, "get_source_expressions")
|
|
98
|
-
and value.get_source_expressions()
|
|
99
|
-
)
|
|
100
|
-
)
|
|
101
|
-
if is_complex:
|
|
102
|
-
complex_fields[field_name] = value
|
|
103
|
-
else:
|
|
104
|
-
simple_fields[field_name] = value
|
|
105
|
-
has_subquery = bool(complex_fields)
|
|
67
|
+
# Check if any of the update values are Subquery objects
|
|
68
|
+
has_subquery = any(
|
|
69
|
+
hasattr(value, "query") and hasattr(value, "resolve_expression")
|
|
70
|
+
for value in kwargs.values()
|
|
71
|
+
)
|
|
106
72
|
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
for row in computed_rows:
|
|
127
|
-
pk = row["pk"]
|
|
128
|
-
field_values = {}
|
|
129
|
-
for fname in complex_fields.keys():
|
|
130
|
-
alias = f"__computed_{fname}"
|
|
131
|
-
field_values[fname] = row.get(alias)
|
|
132
|
-
computed_map[pk] = field_values
|
|
133
|
-
|
|
134
|
-
for instance in instances:
|
|
135
|
-
values_for_instance = computed_map.get(instance.pk, {})
|
|
136
|
-
for fname, fval in values_for_instance.items():
|
|
137
|
-
setattr(instance, fname, fval)
|
|
138
|
-
|
|
139
|
-
# Apply simple values directly
|
|
140
|
-
if simple_fields:
|
|
141
|
-
for obj in instances:
|
|
142
|
-
for field, value in simple_fields.items():
|
|
143
|
-
setattr(obj, field, value)
|
|
144
|
-
|
|
145
|
-
# Run BEFORE_UPDATE hooks with updated instances
|
|
73
|
+
# Apply field updates to instances
|
|
74
|
+
for obj in instances:
|
|
75
|
+
for field, value in kwargs.items():
|
|
76
|
+
setattr(obj, field, value)
|
|
77
|
+
|
|
78
|
+
# Check if we're in a bulk operation context to prevent double hook execution
|
|
79
|
+
from django_bulk_hooks.context import get_bypass_hooks
|
|
80
|
+
current_bypass_hooks = get_bypass_hooks()
|
|
81
|
+
|
|
82
|
+
# If we're in a bulk operation context, skip hooks to prevent double execution
|
|
83
|
+
if current_bypass_hooks:
|
|
84
|
+
logger.debug("update: skipping hooks (bulk context)")
|
|
85
|
+
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
86
|
+
else:
|
|
87
|
+
logger.debug("update: running hooks (standalone)")
|
|
88
|
+
ctx = HookContext(model_cls, bypass_hooks=False)
|
|
89
|
+
# Run validation hooks first
|
|
90
|
+
engine.run(model_cls, VALIDATE_UPDATE, instances, originals, ctx=ctx)
|
|
91
|
+
# Then run BEFORE_UPDATE hooks
|
|
146
92
|
engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
|
|
147
93
|
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if parent._meta.concrete_model is not m._meta.concrete_model:
|
|
152
|
-
return True
|
|
153
|
-
return False
|
|
94
|
+
# Use Django's built-in update logic directly
|
|
95
|
+
# Call the base QuerySet implementation to avoid recursion
|
|
96
|
+
update_count = super().update(**kwargs)
|
|
154
97
|
|
|
155
|
-
|
|
98
|
+
# If we used Subquery objects, refresh the instances to get computed values
|
|
99
|
+
if has_subquery and instances:
|
|
100
|
+
# Simple refresh of model fields without fetching related objects
|
|
101
|
+
# Subquery updates only affect the model's own fields, not relationships
|
|
102
|
+
refreshed_instances = {
|
|
103
|
+
obj.pk: obj for obj in model_cls._base_manager.filter(pk__in=pks)
|
|
104
|
+
}
|
|
156
105
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
instances = [updated_map.get(obj.pk, obj) for obj in instances]
|
|
170
|
-
else:
|
|
171
|
-
# Simple updates on single-table models
|
|
172
|
-
base_manager = model_cls._base_manager
|
|
173
|
-
fields_to_update = list(kwargs.keys())
|
|
174
|
-
base_manager.bulk_update(instances, fields_to_update)
|
|
175
|
-
result = len(instances)
|
|
106
|
+
# Bulk update all instances in memory
|
|
107
|
+
for instance in instances:
|
|
108
|
+
if instance.pk in refreshed_instances:
|
|
109
|
+
refreshed_instance = refreshed_instances[instance.pk]
|
|
110
|
+
# Update all fields except primary key
|
|
111
|
+
for field in model_cls._meta.fields:
|
|
112
|
+
if field.name != "id":
|
|
113
|
+
setattr(
|
|
114
|
+
instance,
|
|
115
|
+
field.name,
|
|
116
|
+
getattr(refreshed_instance, field.name),
|
|
117
|
+
)
|
|
176
118
|
|
|
177
|
-
# Run AFTER_UPDATE hooks only
|
|
178
|
-
if not
|
|
179
|
-
|
|
119
|
+
# Run AFTER_UPDATE hooks only for standalone updates
|
|
120
|
+
if not current_bypass_hooks:
|
|
121
|
+
logger.debug("update: running AFTER_UPDATE")
|
|
180
122
|
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
181
|
-
|
|
182
|
-
|
|
123
|
+
else:
|
|
124
|
+
logger.debug("update: skipping AFTER_UPDATE (bulk context)")
|
|
125
|
+
|
|
126
|
+
return update_count
|
|
183
127
|
|
|
184
128
|
@transaction.atomic
|
|
185
129
|
def bulk_create(
|
|
@@ -192,34 +136,37 @@ class HookQuerySetMixin:
|
|
|
192
136
|
unique_fields=None,
|
|
193
137
|
bypass_hooks=False,
|
|
194
138
|
bypass_validation=False,
|
|
195
|
-
)
|
|
139
|
+
):
|
|
196
140
|
"""
|
|
197
|
-
Insert each of the instances into the database
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
VALIDATE_CREATE → BEFORE_CREATE → DB create → AFTER_CREATE
|
|
201
|
-
|
|
202
|
-
Behaves like Django's bulk_create but supports multi-table inheritance (MTI)
|
|
203
|
-
models and hooks. All arguments are supported and passed through to the correct logic.
|
|
141
|
+
Insert each of the instances into the database. Behaves like Django's bulk_create,
|
|
142
|
+
but supports multi-table inheritance (MTI) models and hooks. All arguments are supported and
|
|
143
|
+
passed through to the correct logic. For MTI, only a subset of options may be supported.
|
|
204
144
|
"""
|
|
205
145
|
model_cls = self.model
|
|
206
146
|
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
|
|
147
|
+
# When you bulk insert you don't get the primary keys back (if it's an
|
|
148
|
+
# autoincrement, except if can_return_rows_from_bulk_insert=True), so
|
|
149
|
+
# you can't insert into the child tables which references this. There
|
|
150
|
+
# are two workarounds:
|
|
151
|
+
# 1) This could be implemented if you didn't have an autoincrement pk
|
|
152
|
+
# 2) You could do it by doing O(n) normal inserts into the parent
|
|
153
|
+
# tables to get the primary keys back and then doing a single bulk
|
|
154
|
+
# insert into the childmost table.
|
|
155
|
+
# We currently set the primary keys on the objects when using
|
|
156
|
+
# PostgreSQL via the RETURNING ID clause. It should be possible for
|
|
157
|
+
# Oracle as well, but the semantics for extracting the primary keys is
|
|
158
|
+
# trickier so it's not done yet.
|
|
159
|
+
if batch_size is not None and batch_size <= 0:
|
|
160
|
+
raise ValueError("Batch size must be a positive integer.")
|
|
210
161
|
|
|
211
162
|
if not objs:
|
|
212
163
|
return objs
|
|
213
164
|
|
|
214
165
|
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
215
166
|
raise TypeError(
|
|
216
|
-
f"bulk_create expected instances of {model_cls.__name__}, "
|
|
217
|
-
f"but got {set(type(obj).__name__ for obj in objs)}"
|
|
167
|
+
f"bulk_create expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
218
168
|
)
|
|
219
169
|
|
|
220
|
-
if batch_size is not None and batch_size <= 0:
|
|
221
|
-
raise ValueError("batch_size must be a positive integer.")
|
|
222
|
-
|
|
223
170
|
# Check for MTI - if we detect multi-table inheritance, we need special handling
|
|
224
171
|
# This follows Django's approach: check that the parents share the same concrete model
|
|
225
172
|
# with our model to detect the inheritance pattern ConcreteGrandParent ->
|
|
@@ -233,12 +180,12 @@ class HookQuerySetMixin:
|
|
|
233
180
|
|
|
234
181
|
# Fire hooks before DB ops
|
|
235
182
|
if not bypass_hooks:
|
|
236
|
-
ctx = HookContext(model_cls, bypass_hooks=False)
|
|
183
|
+
ctx = HookContext(model_cls, bypass_hooks=False) # Pass bypass_hooks
|
|
237
184
|
if not bypass_validation:
|
|
238
185
|
engine.run(model_cls, VALIDATE_CREATE, objs, ctx=ctx)
|
|
239
186
|
engine.run(model_cls, BEFORE_CREATE, objs, ctx=ctx)
|
|
240
187
|
else:
|
|
241
|
-
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
188
|
+
ctx = HookContext(model_cls, bypass_hooks=True) # Pass bypass_hooks
|
|
242
189
|
logger.debug("bulk_create bypassed hooks")
|
|
243
190
|
|
|
244
191
|
# For MTI models, we need to handle them specially
|
|
@@ -280,129 +227,74 @@ class HookQuerySetMixin:
|
|
|
280
227
|
@transaction.atomic
|
|
281
228
|
def bulk_update(
|
|
282
229
|
self, objs, fields, bypass_hooks=False, bypass_validation=False, **kwargs
|
|
283
|
-
)
|
|
230
|
+
):
|
|
284
231
|
"""
|
|
285
|
-
Bulk update objects in the database with
|
|
286
|
-
|
|
287
|
-
This method always runs the complete hook cycle:
|
|
288
|
-
VALIDATE_UPDATE → BEFORE_UPDATE → DB update → AFTER_UPDATE
|
|
289
|
-
|
|
290
|
-
Args:
|
|
291
|
-
objs: List of model instances to update
|
|
292
|
-
fields: List of field names to update
|
|
293
|
-
bypass_hooks: DEPRECATED - kept for backward compatibility only
|
|
294
|
-
bypass_validation: DEPRECATED - kept for backward compatibility only
|
|
295
|
-
**kwargs: Additional arguments passed to Django's bulk_update
|
|
232
|
+
Bulk update objects in the database with MTI support.
|
|
296
233
|
"""
|
|
297
234
|
model_cls = self.model
|
|
298
235
|
|
|
299
236
|
if not objs:
|
|
300
237
|
return []
|
|
301
238
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if not isinstance(fields, (list, tuple)):
|
|
307
|
-
raise TypeError("fields must be a list or tuple")
|
|
308
|
-
|
|
309
|
-
if not objs:
|
|
310
|
-
return []
|
|
311
|
-
|
|
312
|
-
if not fields:
|
|
313
|
-
raise ValueError("fields cannot be empty")
|
|
314
|
-
|
|
315
|
-
# Validate that all objects are instances of the model
|
|
316
|
-
for obj in objs:
|
|
317
|
-
if not isinstance(obj, model_cls):
|
|
318
|
-
raise TypeError(
|
|
319
|
-
f"Expected instances of {model_cls.__name__}, got {type(obj).__name__}"
|
|
320
|
-
)
|
|
321
|
-
if obj.pk is None:
|
|
322
|
-
raise ValueError("All objects must have a primary key")
|
|
323
|
-
|
|
324
|
-
# Load originals for hook comparison
|
|
325
|
-
pks = [obj.pk for obj in objs]
|
|
326
|
-
original_map = {
|
|
327
|
-
obj.pk: obj for obj in model_cls._base_manager.filter(pk__in=pks)
|
|
328
|
-
}
|
|
329
|
-
originals = [original_map.get(obj.pk) for obj in objs]
|
|
330
|
-
|
|
331
|
-
# Run VALIDATE_UPDATE hooks
|
|
332
|
-
if not bypass_validation:
|
|
333
|
-
ctx = HookContext(model_cls)
|
|
334
|
-
engine.run(
|
|
335
|
-
model_cls, VALIDATE_UPDATE, objs, originals, ctx=ctx
|
|
239
|
+
if any(not isinstance(obj, model_cls) for obj in objs):
|
|
240
|
+
raise TypeError(
|
|
241
|
+
f"bulk_update expected instances of {model_cls.__name__}, but got {set(type(obj).__name__ for obj in objs)}"
|
|
336
242
|
)
|
|
337
243
|
|
|
338
|
-
|
|
339
|
-
if not bypass_hooks:
|
|
340
|
-
ctx = HookContext(model_cls)
|
|
341
|
-
engine.run(
|
|
342
|
-
model_cls, BEFORE_UPDATE, objs, originals, ctx=ctx
|
|
343
|
-
)
|
|
244
|
+
logger.debug(f"bulk_update {model_cls.__name__} bypass_hooks={bypass_hooks} objs={len(objs)}")
|
|
344
245
|
|
|
345
|
-
#
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
246
|
+
# Check for MTI
|
|
247
|
+
is_mti = False
|
|
248
|
+
for parent in model_cls._meta.all_parents:
|
|
249
|
+
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
250
|
+
is_mti = True
|
|
251
|
+
break
|
|
351
252
|
|
|
352
|
-
if
|
|
353
|
-
|
|
253
|
+
if not bypass_hooks:
|
|
254
|
+
logger.debug("bulk_update: hooks will run in update()")
|
|
255
|
+
ctx = HookContext(model_cls, bypass_hooks=False)
|
|
256
|
+
originals = [None] * len(objs) # Placeholder for after_update call
|
|
257
|
+
else:
|
|
258
|
+
logger.debug("bulk_update: hooks bypassed")
|
|
259
|
+
ctx = HookContext(model_cls, bypass_hooks=True)
|
|
260
|
+
originals = [None] * len(objs) # Ensure originals is defined for after_update call
|
|
261
|
+
|
|
262
|
+
# Handle auto_now fields like Django's update_or_create does
|
|
263
|
+
fields_set = set(fields)
|
|
264
|
+
pk_fields = model_cls._meta.pk_fields
|
|
265
|
+
for field in model_cls._meta.local_concrete_fields:
|
|
266
|
+
# Only add auto_now fields (like updated_at) that aren't already in the fields list
|
|
267
|
+
# Don't include auto_now_add fields (like created_at) as they should only be set on creation
|
|
268
|
+
if hasattr(field, "auto_now") and field.auto_now:
|
|
269
|
+
if field.name not in fields_set and field.name not in pk_fields:
|
|
270
|
+
fields_set.add(field.name)
|
|
271
|
+
if field.name != field.attname:
|
|
272
|
+
fields_set.add(field.attname)
|
|
273
|
+
fields = list(fields_set)
|
|
274
|
+
|
|
275
|
+
# Handle MTI models differently
|
|
276
|
+
if is_mti:
|
|
354
277
|
result = self._mti_bulk_update(objs, fields, **kwargs)
|
|
355
278
|
else:
|
|
356
|
-
#
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
279
|
+
# For single-table models, use Django's built-in bulk_update
|
|
280
|
+
django_kwargs = {
|
|
281
|
+
k: v
|
|
282
|
+
for k, v in kwargs.items()
|
|
283
|
+
if k not in ["bypass_hooks", "bypass_validation"]
|
|
284
|
+
}
|
|
285
|
+
logger.debug("Calling Django bulk_update")
|
|
286
|
+
result = super().bulk_update(objs, fields, **django_kwargs)
|
|
287
|
+
logger.debug(f"Django bulk_update done: {result}")
|
|
360
288
|
|
|
361
|
-
#
|
|
289
|
+
# Note: We don't run AFTER_UPDATE hooks here to prevent double execution
|
|
290
|
+
# The update() method will handle all hook execution based on thread-local state
|
|
362
291
|
if not bypass_hooks:
|
|
363
|
-
|
|
364
|
-
|
|
292
|
+
logger.debug("bulk_update: skipping AFTER_UPDATE (update() will handle)")
|
|
293
|
+
else:
|
|
294
|
+
logger.debug("bulk_update: hooks bypassed")
|
|
365
295
|
|
|
366
296
|
return result
|
|
367
297
|
|
|
368
|
-
@transaction.atomic
|
|
369
|
-
def bulk_delete(self, objs, **kwargs) -> int:
|
|
370
|
-
"""
|
|
371
|
-
Delete the given objects from the database with complete hook support.
|
|
372
|
-
|
|
373
|
-
This method runs the complete hook cycle:
|
|
374
|
-
VALIDATE_DELETE → BEFORE_DELETE → DB delete → AFTER_DELETE
|
|
375
|
-
|
|
376
|
-
This is a convenience method that provides a bulk_delete interface
|
|
377
|
-
similar to bulk_create and bulk_update.
|
|
378
|
-
"""
|
|
379
|
-
model_cls = self.model
|
|
380
|
-
|
|
381
|
-
# Extract custom kwargs
|
|
382
|
-
kwargs.pop("bypass_hooks", False)
|
|
383
|
-
|
|
384
|
-
# Validate inputs
|
|
385
|
-
if not isinstance(objs, (list, tuple)):
|
|
386
|
-
raise TypeError("objs must be a list or tuple")
|
|
387
|
-
|
|
388
|
-
if not objs:
|
|
389
|
-
return 0
|
|
390
|
-
|
|
391
|
-
# Validate that all objects are instances of the model
|
|
392
|
-
for obj in objs:
|
|
393
|
-
if not isinstance(obj, model_cls):
|
|
394
|
-
raise TypeError(
|
|
395
|
-
f"Expected instances of {model_cls.__name__}, got {type(obj).__name__}"
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
# Get the pks to delete
|
|
399
|
-
pks = [obj.pk for obj in objs if obj.pk is not None]
|
|
400
|
-
if not pks:
|
|
401
|
-
return 0
|
|
402
|
-
|
|
403
|
-
# Use the delete() method which already has hook support
|
|
404
|
-
return self.filter(pk__in=pks).delete()
|
|
405
|
-
|
|
406
298
|
def _detect_modified_fields(self, new_instances, original_instances):
|
|
407
299
|
"""
|
|
408
300
|
Detect fields that were modified during BEFORE_UPDATE hooks by comparing
|
|
@@ -504,83 +396,50 @@ class HookQuerySetMixin:
|
|
|
504
396
|
# Then we can use Django's bulk_create for the child objects
|
|
505
397
|
parent_objects_map = {}
|
|
506
398
|
|
|
507
|
-
# Step 1:
|
|
399
|
+
# Step 1: Do O(n) normal inserts into parent tables to get primary keys back
|
|
400
|
+
# Get bypass_hooks from kwargs
|
|
508
401
|
bypass_hooks = kwargs.get("bypass_hooks", False)
|
|
509
402
|
bypass_validation = kwargs.get("bypass_validation", False)
|
|
510
403
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
if supports_returning:
|
|
515
|
-
# For each parent level in the chain, create instances in batch preserving order
|
|
516
|
-
current_parents_per_obj = {id(obj): None for obj in batch}
|
|
404
|
+
for obj in batch:
|
|
405
|
+
parent_instances = {}
|
|
406
|
+
current_parent = None
|
|
517
407
|
for model_class in inheritance_chain[:-1]:
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
]
|
|
408
|
+
parent_obj = self._create_parent_instance(
|
|
409
|
+
obj, model_class, current_parent
|
|
410
|
+
)
|
|
522
411
|
|
|
412
|
+
# Fire parent hooks if not bypassed
|
|
523
413
|
if not bypass_hooks:
|
|
524
414
|
ctx = HookContext(model_class)
|
|
525
415
|
if not bypass_validation:
|
|
526
|
-
engine.run(model_class, VALIDATE_CREATE,
|
|
527
|
-
engine.run(model_class, BEFORE_CREATE,
|
|
528
|
-
|
|
529
|
-
#
|
|
530
|
-
|
|
531
|
-
|
|
416
|
+
engine.run(model_class, VALIDATE_CREATE, [parent_obj], ctx=ctx)
|
|
417
|
+
engine.run(model_class, BEFORE_CREATE, [parent_obj], ctx=ctx)
|
|
418
|
+
|
|
419
|
+
# Use Django's base manager to create the object and get PKs back
|
|
420
|
+
# This bypasses hooks and the MTI exception
|
|
421
|
+
field_values = {
|
|
422
|
+
field.name: getattr(parent_obj, field.name)
|
|
423
|
+
for field in model_class._meta.local_fields
|
|
424
|
+
if hasattr(parent_obj, field.name)
|
|
425
|
+
and getattr(parent_obj, field.name) is not None
|
|
426
|
+
}
|
|
427
|
+
created_obj = model_class._base_manager.using(self.db).create(
|
|
428
|
+
**field_values
|
|
532
429
|
)
|
|
533
430
|
|
|
534
|
-
#
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
# Update maps and state for next parent level
|
|
539
|
-
for obj, parent_obj in zip(batch, created_parents):
|
|
540
|
-
# Ensure state reflects saved
|
|
541
|
-
parent_obj._state.adding = False
|
|
542
|
-
parent_obj._state.db = self.db
|
|
543
|
-
# Record for this object and level
|
|
544
|
-
if id(obj) not in parent_objects_map:
|
|
545
|
-
parent_objects_map[id(obj)] = {}
|
|
546
|
-
parent_objects_map[id(obj)][model_class] = parent_obj
|
|
547
|
-
current_parents_per_obj[id(obj)] = parent_obj
|
|
548
|
-
else:
|
|
549
|
-
# Fallback: per-row parent inserts (original behavior)
|
|
550
|
-
for obj in batch:
|
|
551
|
-
parent_instances = {}
|
|
552
|
-
current_parent = None
|
|
553
|
-
for model_class in inheritance_chain[:-1]:
|
|
554
|
-
parent_obj = self._create_parent_instance(
|
|
555
|
-
obj, model_class, current_parent
|
|
556
|
-
)
|
|
557
|
-
|
|
558
|
-
if not bypass_hooks:
|
|
559
|
-
ctx = HookContext(model_class)
|
|
560
|
-
if not bypass_validation:
|
|
561
|
-
engine.run(model_class, VALIDATE_CREATE, [parent_obj], ctx=ctx)
|
|
562
|
-
engine.run(model_class, BEFORE_CREATE, [parent_obj], ctx=ctx)
|
|
563
|
-
|
|
564
|
-
field_values = {
|
|
565
|
-
field.name: getattr(parent_obj, field.name)
|
|
566
|
-
for field in model_class._meta.local_fields
|
|
567
|
-
if hasattr(parent_obj, field.name)
|
|
568
|
-
and getattr(parent_obj, field.name) is not None
|
|
569
|
-
}
|
|
570
|
-
created_obj = model_class._base_manager.using(self.db).create(
|
|
571
|
-
**field_values
|
|
572
|
-
)
|
|
573
|
-
|
|
574
|
-
parent_obj.pk = created_obj.pk
|
|
575
|
-
parent_obj._state.adding = False
|
|
576
|
-
parent_obj._state.db = self.db
|
|
431
|
+
# Update the parent_obj with the created object's PK
|
|
432
|
+
parent_obj.pk = created_obj.pk
|
|
433
|
+
parent_obj._state.adding = False
|
|
434
|
+
parent_obj._state.db = self.db
|
|
577
435
|
|
|
578
|
-
|
|
579
|
-
|
|
436
|
+
# Fire AFTER_CREATE hooks for parent
|
|
437
|
+
if not bypass_hooks:
|
|
438
|
+
engine.run(model_class, AFTER_CREATE, [parent_obj], ctx=ctx)
|
|
580
439
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
440
|
+
parent_instances[model_class] = parent_obj
|
|
441
|
+
current_parent = parent_obj
|
|
442
|
+
parent_objects_map[id(obj)] = parent_instances
|
|
584
443
|
|
|
585
444
|
# Step 2: Create all child objects and do single bulk insert into childmost table
|
|
586
445
|
child_model = inheritance_chain[-1]
|
|
@@ -806,8 +665,7 @@ class HookQuerySetMixin:
|
|
|
806
665
|
# For MTI, we need to handle parent links correctly
|
|
807
666
|
# The root model (first in chain) has its own PK
|
|
808
667
|
# Child models use the parent link to reference the root PK
|
|
809
|
-
|
|
810
|
-
# root_model = inheritance_chain[0]
|
|
668
|
+
root_model = inheritance_chain[0]
|
|
811
669
|
|
|
812
670
|
# Get the primary keys from the objects
|
|
813
671
|
# If objects have pk set but are not loaded from DB, use those PKs
|
|
@@ -891,7 +749,7 @@ class HookQuerySetMixin:
|
|
|
891
749
|
**{f"{filter_field}__in": pks}
|
|
892
750
|
).update(**case_statements)
|
|
893
751
|
total_updated += updated_count
|
|
894
|
-
except Exception:
|
|
752
|
+
except Exception as e:
|
|
895
753
|
import traceback
|
|
896
754
|
|
|
897
755
|
traceback.print_exc()
|