django-bulk-hooks 0.1.125__tar.gz → 0.1.127__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-bulk-hooks might be problematic. Click here for more details.
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/PKG-INFO +1 -1
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/django_bulk_hooks/queryset.py +65 -49
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/pyproject.toml +1 -1
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/LICENSE +0 -0
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/README.md +0 -0
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/django_bulk_hooks/engine.py +0 -0
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/django_bulk_hooks/priority.py +0 -0
- {django_bulk_hooks-0.1.125 → django_bulk_hooks-0.1.127}/django_bulk_hooks/registry.py +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
from django.db import models, transaction
|
|
3
2
|
from django.db.models import AutoField
|
|
4
3
|
|
|
@@ -44,7 +43,7 @@ class HookQuerySet(models.QuerySet):
|
|
|
44
43
|
for obj in instances:
|
|
45
44
|
for field, value in kwargs.items():
|
|
46
45
|
setattr(obj, field, value)
|
|
47
|
-
|
|
46
|
+
|
|
48
47
|
# Run BEFORE_UPDATE hooks
|
|
49
48
|
ctx = HookContext(model_cls)
|
|
50
49
|
engine.run(model_cls, BEFORE_UPDATE, instances, originals, ctx=ctx)
|
|
@@ -77,10 +76,26 @@ class HookQuerySet(models.QuerySet):
|
|
|
77
76
|
"""
|
|
78
77
|
model_cls = self.model
|
|
79
78
|
|
|
79
|
+
# When you bulk insert you don't get the primary keys back (if it's an
|
|
80
|
+
# autoincrement, except if can_return_rows_from_bulk_insert=True), so
|
|
81
|
+
# you can't insert into the child tables which references this. There
|
|
82
|
+
# are two workarounds:
|
|
83
|
+
# 1) This could be implemented if you didn't have an autoincrement pk
|
|
84
|
+
# 2) You could do it by doing O(n) normal inserts into the parent
|
|
85
|
+
# tables to get the primary keys back and then doing a single bulk
|
|
86
|
+
# insert into the childmost table.
|
|
87
|
+
# We currently set the primary keys on the objects when using
|
|
88
|
+
# PostgreSQL via the RETURNING ID clause. It should be possible for
|
|
89
|
+
# Oracle as well, but the semantics for extracting the primary keys is
|
|
90
|
+
# trickier so it's not done yet.
|
|
80
91
|
if batch_size is not None and batch_size <= 0:
|
|
81
92
|
raise ValueError("Batch size must be a positive integer.")
|
|
82
93
|
|
|
83
94
|
# Check for MTI - if we detect multi-table inheritance, we need special handling
|
|
95
|
+
# This follows Django's approach: check that the parents share the same concrete model
|
|
96
|
+
# with our model to detect the inheritance pattern ConcreteGrandParent ->
|
|
97
|
+
# MultiTableParent -> ProxyChild. Simply checking self.model._meta.proxy would not
|
|
98
|
+
# identify that case as involving multiple tables.
|
|
84
99
|
is_mti = False
|
|
85
100
|
for parent in model_cls._meta.all_parents:
|
|
86
101
|
if parent._meta.concrete_model is not model_cls._meta.concrete_model:
|
|
@@ -116,7 +131,7 @@ class HookQuerySet(models.QuerySet):
|
|
|
116
131
|
else:
|
|
117
132
|
# For single-table models, use Django's built-in bulk_create
|
|
118
133
|
# but we need to call it on the base manager to avoid recursion
|
|
119
|
-
|
|
134
|
+
|
|
120
135
|
result = model_cls._base_manager.bulk_create(
|
|
121
136
|
objs,
|
|
122
137
|
batch_size=batch_size,
|
|
@@ -172,7 +187,7 @@ class HookQuerySet(models.QuerySet):
|
|
|
172
187
|
|
|
173
188
|
for i in range(0, len(objs), self.CHUNK_SIZE):
|
|
174
189
|
chunk = objs[i : i + self.CHUNK_SIZE]
|
|
175
|
-
|
|
190
|
+
|
|
176
191
|
# Call the base implementation to avoid re-triggering this method
|
|
177
192
|
super().bulk_update(chunk, fields, **kwargs)
|
|
178
193
|
|
|
@@ -182,7 +197,9 @@ class HookQuerySet(models.QuerySet):
|
|
|
182
197
|
return objs
|
|
183
198
|
|
|
184
199
|
@transaction.atomic
|
|
185
|
-
def bulk_delete(
|
|
200
|
+
def bulk_delete(
|
|
201
|
+
self, objs, batch_size=None, bypass_hooks=False, bypass_validation=False
|
|
202
|
+
):
|
|
186
203
|
if not objs:
|
|
187
204
|
return []
|
|
188
205
|
|
|
@@ -214,8 +231,6 @@ class HookQuerySet(models.QuerySet):
|
|
|
214
231
|
|
|
215
232
|
return objs
|
|
216
233
|
|
|
217
|
-
# --- Private helper methods ---
|
|
218
|
-
|
|
219
234
|
def _detect_modified_fields(self, new_instances, original_instances):
|
|
220
235
|
"""
|
|
221
236
|
Detect fields that were modified during BEFORE_UPDATE hooks by comparing
|
|
@@ -274,16 +289,20 @@ class HookQuerySet(models.QuerySet):
|
|
|
274
289
|
|
|
275
290
|
def _mti_bulk_create(self, objs, inheritance_chain=None, **kwargs):
|
|
276
291
|
"""
|
|
277
|
-
Implements workaround
|
|
292
|
+
Implements Django's suggested workaround #2 for MTI bulk_create:
|
|
293
|
+
O(n) normal inserts into parent tables to get primary keys back,
|
|
294
|
+
then single bulk insert into childmost table.
|
|
278
295
|
Sets auto_now_add/auto_now fields for each model in the chain.
|
|
279
296
|
"""
|
|
280
297
|
if inheritance_chain is None:
|
|
281
298
|
inheritance_chain = self._get_inheritance_chain()
|
|
282
|
-
|
|
299
|
+
|
|
283
300
|
# Safety check to prevent infinite recursion
|
|
284
301
|
if len(inheritance_chain) > 10: # Arbitrary limit to prevent infinite loops
|
|
285
|
-
raise ValueError(
|
|
286
|
-
|
|
302
|
+
raise ValueError(
|
|
303
|
+
"Inheritance chain too deep - possible infinite recursion detected"
|
|
304
|
+
)
|
|
305
|
+
|
|
287
306
|
batch_size = kwargs.get("batch_size") or len(objs)
|
|
288
307
|
created_objects = []
|
|
289
308
|
with transaction.atomic(using=self.db, savepoint=False):
|
|
@@ -298,13 +317,14 @@ class HookQuerySet(models.QuerySet):
|
|
|
298
317
|
def _process_mti_batch(self, batch, inheritance_chain, **kwargs):
|
|
299
318
|
"""
|
|
300
319
|
Process a single batch of objects through the inheritance chain.
|
|
301
|
-
|
|
320
|
+
Implements Django's suggested workaround #2: O(n) normal inserts into parent
|
|
321
|
+
tables to get primary keys back, then single bulk insert into childmost table.
|
|
302
322
|
"""
|
|
303
323
|
# For MTI, we need to save parent objects first to get PKs
|
|
304
324
|
# Then we can use Django's bulk_create for the child objects
|
|
305
325
|
parent_objects_map = {}
|
|
306
|
-
|
|
307
|
-
# Step 1:
|
|
326
|
+
|
|
327
|
+
# Step 1: Do O(n) normal inserts into parent tables to get primary keys back
|
|
308
328
|
for obj in batch:
|
|
309
329
|
parent_instances = {}
|
|
310
330
|
current_parent = None
|
|
@@ -312,33 +332,34 @@ class HookQuerySet(models.QuerySet):
|
|
|
312
332
|
parent_obj = self._create_parent_instance(
|
|
313
333
|
obj, model_class, current_parent
|
|
314
334
|
)
|
|
315
|
-
# Use Django's internal
|
|
316
|
-
|
|
335
|
+
# Use Django's internal _insert method to get PKs back
|
|
336
|
+
# This bypasses hooks and the MTI exception
|
|
337
|
+
parent_obj._do_insert(parent_obj._meta, using=self.db)
|
|
317
338
|
parent_instances[model_class] = parent_obj
|
|
318
339
|
current_parent = parent_obj
|
|
319
340
|
parent_objects_map[id(obj)] = parent_instances
|
|
320
|
-
|
|
321
|
-
# Step 2: Create and bulk insert
|
|
341
|
+
|
|
342
|
+
# Step 2: Create all child objects and do single bulk insert into childmost table
|
|
322
343
|
child_model = inheritance_chain[-1]
|
|
323
|
-
|
|
344
|
+
all_child_objects = []
|
|
324
345
|
for obj in batch:
|
|
325
346
|
child_obj = self._create_child_instance(
|
|
326
347
|
obj, child_model, parent_objects_map.get(id(obj), {})
|
|
327
348
|
)
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
#
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
349
|
+
all_child_objects.append(child_obj)
|
|
350
|
+
|
|
351
|
+
# Step 2.5: Single bulk insert into childmost table
|
|
352
|
+
if all_child_objects:
|
|
353
|
+
# Use Django's internal bulk_create to bypass MTI exception
|
|
354
|
+
child_model._base_manager.bulk_create(all_child_objects)
|
|
355
|
+
|
|
335
356
|
# Step 3: Update original objects with generated PKs and state
|
|
336
357
|
pk_field_name = child_model._meta.pk.name
|
|
337
|
-
for orig_obj, child_obj in zip(batch,
|
|
358
|
+
for orig_obj, child_obj in zip(batch, all_child_objects):
|
|
338
359
|
setattr(orig_obj, pk_field_name, getattr(child_obj, pk_field_name))
|
|
339
360
|
orig_obj._state.adding = False
|
|
340
361
|
orig_obj._state.db = self.db
|
|
341
|
-
|
|
362
|
+
|
|
342
363
|
return batch
|
|
343
364
|
|
|
344
365
|
def _create_parent_instance(self, source_obj, parent_model, current_parent):
|
|
@@ -358,20 +379,18 @@ class HookQuerySet(models.QuerySet):
|
|
|
358
379
|
):
|
|
359
380
|
setattr(parent_obj, field.name, current_parent)
|
|
360
381
|
break
|
|
361
|
-
|
|
382
|
+
|
|
362
383
|
# Handle auto_now_add and auto_now fields like Django does
|
|
363
384
|
for field in parent_model._meta.local_fields:
|
|
364
|
-
if hasattr(field,
|
|
365
|
-
|
|
366
|
-
elif hasattr(field, 'auto_now') and field.auto_now:
|
|
367
|
-
field.pre_save(parent_obj, add=True)
|
|
368
|
-
|
|
369
|
-
# Ensure auto_now_add fields are explicitly set to prevent null constraint violations
|
|
370
|
-
for field in parent_model._meta.local_fields:
|
|
371
|
-
if hasattr(field, 'auto_now_add') and field.auto_now_add:
|
|
385
|
+
if hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
386
|
+
# Ensure auto_now_add fields are properly set
|
|
372
387
|
if getattr(parent_obj, field.name) is None:
|
|
373
388
|
field.pre_save(parent_obj, add=True)
|
|
374
|
-
|
|
389
|
+
# Explicitly set the value to ensure it's not None
|
|
390
|
+
setattr(parent_obj, field.name, field.value_from_object(parent_obj))
|
|
391
|
+
elif hasattr(field, "auto_now") and field.auto_now:
|
|
392
|
+
field.pre_save(parent_obj, add=True)
|
|
393
|
+
|
|
375
394
|
return parent_obj
|
|
376
395
|
|
|
377
396
|
def _create_child_instance(self, source_obj, child_model, parent_instances):
|
|
@@ -387,19 +406,16 @@ class HookQuerySet(models.QuerySet):
|
|
|
387
406
|
parent_link = child_model._meta.get_ancestor_link(parent_model)
|
|
388
407
|
if parent_link:
|
|
389
408
|
setattr(child_obj, parent_link.name, parent_instance)
|
|
390
|
-
|
|
409
|
+
|
|
391
410
|
# Handle auto_now_add and auto_now fields like Django does
|
|
392
411
|
for field in child_model._meta.local_fields:
|
|
393
|
-
if hasattr(field,
|
|
394
|
-
|
|
395
|
-
elif hasattr(field, 'auto_now') and field.auto_now:
|
|
396
|
-
field.pre_save(child_obj, add=True)
|
|
397
|
-
|
|
398
|
-
# Ensure auto_now_add fields are explicitly set to prevent null constraint violations
|
|
399
|
-
for field in child_model._meta.local_fields:
|
|
400
|
-
if hasattr(field, 'auto_now_add') and field.auto_now_add:
|
|
412
|
+
if hasattr(field, "auto_now_add") and field.auto_now_add:
|
|
413
|
+
# Ensure auto_now_add fields are properly set
|
|
401
414
|
if getattr(child_obj, field.name) is None:
|
|
402
415
|
field.pre_save(child_obj, add=True)
|
|
403
|
-
|
|
404
|
-
|
|
416
|
+
# Explicitly set the value to ensure it's not None
|
|
417
|
+
setattr(child_obj, field.name, field.value_from_object(child_obj))
|
|
418
|
+
elif hasattr(field, "auto_now") and field.auto_now:
|
|
419
|
+
field.pre_save(child_obj, add=True)
|
|
405
420
|
|
|
421
|
+
return child_obj
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "django-bulk-hooks"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.127"
|
|
4
4
|
description = "Hook-style hooks for Django bulk operations like bulk_create and bulk_update."
|
|
5
5
|
authors = ["Konrad Beck <konrad.beck@merchantcapital.co.za>"]
|
|
6
6
|
readme = "README.md"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|