django-bulk-hooks 0.2.55__py3-none-any.whl → 0.2.57__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/operations/bulk_executor.py +52 -2
- django_bulk_hooks/operations/mti_handler.py +33 -1
- {django_bulk_hooks-0.2.55.dist-info → django_bulk_hooks-0.2.57.dist-info}/METADATA +1 -1
- {django_bulk_hooks-0.2.55.dist-info → django_bulk_hooks-0.2.57.dist-info}/RECORD +6 -6
- {django_bulk_hooks-0.2.55.dist-info → django_bulk_hooks-0.2.57.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.2.55.dist-info → django_bulk_hooks-0.2.57.dist-info}/WHEEL +0 -0
|
@@ -8,6 +8,7 @@ import logging
|
|
|
8
8
|
|
|
9
9
|
from django.db import transaction
|
|
10
10
|
from django.db.models import AutoField, ForeignKey, Case, When, Value
|
|
11
|
+
from django.db.models.constants import OnConflict
|
|
11
12
|
from django.db.models.functions import Cast
|
|
12
13
|
|
|
13
14
|
from django_bulk_hooks.operations.field_utils import get_field_value_for_db
|
|
@@ -333,15 +334,64 @@ class BulkExecutor:
|
|
|
333
334
|
if objs_without_pk:
|
|
334
335
|
base_qs._prepare_for_bulk_create(objs_without_pk)
|
|
335
336
|
opts = plan.child_model._meta
|
|
336
|
-
|
|
337
|
+
|
|
337
338
|
# Include all local fields except auto-generated ones
|
|
338
339
|
# For MTI, we need to include the parent link (which is the PK)
|
|
339
340
|
filtered_fields = [f for f in opts.local_fields if not f.generated]
|
|
340
|
-
|
|
341
|
+
|
|
342
|
+
# FIX: Pass conflict resolution parameters to _batched_insert for MTI child tables
|
|
343
|
+
# Previously, _batched_insert was called without on_conflict/unique_fields/update_fields,
|
|
344
|
+
# causing IntegrityError when child tables have unique constraints during upsert operations.
|
|
345
|
+
# See: https://github.com/user/repo/issues/XXX
|
|
346
|
+
# Prepare conflict resolution parameters for upsert
|
|
347
|
+
on_conflict = None
|
|
348
|
+
batched_unique_fields = None
|
|
349
|
+
batched_update_fields = None
|
|
350
|
+
|
|
351
|
+
if plan.update_conflicts:
|
|
352
|
+
# Filter unique_fields and update_fields to only those on child model
|
|
353
|
+
# Django's _batched_insert expects field objects, not field names
|
|
354
|
+
child_model_fields_dict = {field.name: field for field in plan.child_model._meta.local_fields}
|
|
355
|
+
|
|
356
|
+
# Unique fields may be on parent or child - filter to child only for child table insert
|
|
357
|
+
# Convert field names to field objects
|
|
358
|
+
if plan.unique_fields:
|
|
359
|
+
batched_unique_fields = [
|
|
360
|
+
child_model_fields_dict[fname]
|
|
361
|
+
for fname in plan.unique_fields
|
|
362
|
+
if fname in child_model_fields_dict
|
|
363
|
+
]
|
|
364
|
+
|
|
365
|
+
# Update fields - filter to child only
|
|
366
|
+
# Keep as strings - Django's _batched_insert accepts field name strings for update_fields
|
|
367
|
+
if plan.update_fields:
|
|
368
|
+
batched_update_fields = [
|
|
369
|
+
fname
|
|
370
|
+
for fname in plan.update_fields
|
|
371
|
+
if fname in child_model_fields_dict
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
# Only set on_conflict if we have unique fields for this table
|
|
375
|
+
# Note: If unique_fields are all on parent, batched_unique_fields will be empty,
|
|
376
|
+
# meaning no conflict resolution needed for child table
|
|
377
|
+
if batched_unique_fields:
|
|
378
|
+
if batched_update_fields:
|
|
379
|
+
# We have both unique fields and update fields on child - use UPDATE
|
|
380
|
+
on_conflict = OnConflict.UPDATE
|
|
381
|
+
else:
|
|
382
|
+
# We have unique fields on child but no update fields - use IGNORE
|
|
383
|
+
# This handles the case where all update fields are on parent tables
|
|
384
|
+
on_conflict = OnConflict.IGNORE
|
|
385
|
+
# Clear batched_update_fields to avoid issues
|
|
386
|
+
batched_update_fields = None
|
|
387
|
+
|
|
341
388
|
returned_columns = base_qs._batched_insert(
|
|
342
389
|
objs_without_pk,
|
|
343
390
|
filtered_fields,
|
|
344
391
|
batch_size=len(objs_without_pk),
|
|
392
|
+
on_conflict=on_conflict,
|
|
393
|
+
update_fields=batched_update_fields,
|
|
394
|
+
unique_fields=batched_unique_fields,
|
|
345
395
|
)
|
|
346
396
|
if returned_columns:
|
|
347
397
|
for obj, results in zip(objs_without_pk, returned_columns):
|
|
@@ -207,6 +207,8 @@ class MTIHandler:
|
|
|
207
207
|
update_conflicts=update_conflicts,
|
|
208
208
|
unique_fields=unique_fields,
|
|
209
209
|
update_fields=update_fields,
|
|
210
|
+
existing_record_ids=existing_record_ids,
|
|
211
|
+
existing_pks_map=existing_pks_map,
|
|
210
212
|
)
|
|
211
213
|
|
|
212
214
|
# Build child object templates (without parent links - executor adds them)
|
|
@@ -235,6 +237,8 @@ class MTIHandler:
|
|
|
235
237
|
update_conflicts=False,
|
|
236
238
|
unique_fields=None,
|
|
237
239
|
update_fields=None,
|
|
240
|
+
existing_record_ids=None,
|
|
241
|
+
existing_pks_map=None,
|
|
238
242
|
):
|
|
239
243
|
"""
|
|
240
244
|
Build parent level objects for each level in the inheritance chain.
|
|
@@ -249,6 +253,12 @@ class MTIHandler:
|
|
|
249
253
|
parent_levels = []
|
|
250
254
|
parent_instances_map = {} # Maps obj id() -> {model_class: parent_instance}
|
|
251
255
|
|
|
256
|
+
# Set defaults
|
|
257
|
+
if existing_record_ids is None:
|
|
258
|
+
existing_record_ids = set()
|
|
259
|
+
if existing_pks_map is None:
|
|
260
|
+
existing_pks_map = {}
|
|
261
|
+
|
|
252
262
|
for level_idx, model_class in enumerate(inheritance_chain[:-1]):
|
|
253
263
|
parent_objs_for_level = []
|
|
254
264
|
|
|
@@ -301,6 +311,28 @@ class MTIHandler:
|
|
|
301
311
|
level_update_conflicts = True
|
|
302
312
|
level_unique_fields = normalized_unique
|
|
303
313
|
level_update_fields = filtered_updates
|
|
314
|
+
else:
|
|
315
|
+
# CRITICAL FIX: Even if this parent level doesn't have the unique constraint,
|
|
316
|
+
# we still need update_conflicts=True during MTI upsert. Without it, parent
|
|
317
|
+
# does plain INSERT creating a new parent record with a new PK, breaking the
|
|
318
|
+
# MTI relationship and causing child INSERT to fail on its unique constraint.
|
|
319
|
+
#
|
|
320
|
+
# Solution: Use the primary key as the unique field for parent levels.
|
|
321
|
+
# In MTI upsert with existing_pks_map, parent objects will get their PKs from
|
|
322
|
+
# bulk_create, and we need those PKs to match existing records, not create new ones.
|
|
323
|
+
if existing_record_ids and existing_pks_map:
|
|
324
|
+
# Use primary key for upsert matching at this level
|
|
325
|
+
pk_field = model_class._meta.pk
|
|
326
|
+
level_update_conflicts = True
|
|
327
|
+
level_unique_fields = [pk_field.name]
|
|
328
|
+
# Use a safe update field - pick the first available non-PK field
|
|
329
|
+
# or use the PK itself as a dummy (updating to itself is a no-op)
|
|
330
|
+
available_fields = [
|
|
331
|
+
f.name
|
|
332
|
+
for f in model_class._meta.local_fields
|
|
333
|
+
if not isinstance(f, AutoField) and f.name in model_fields_by_name
|
|
334
|
+
]
|
|
335
|
+
level_update_fields = available_fields[:1] if available_fields else [pk_field.name]
|
|
304
336
|
|
|
305
337
|
# Create parent level
|
|
306
338
|
parent_level = ParentLevel(
|
|
@@ -367,7 +399,7 @@ class MTIHandler:
|
|
|
367
399
|
# for this level, leading to IntegrityError on primary key constraint.
|
|
368
400
|
if isinstance(field, AutoField):
|
|
369
401
|
continue
|
|
370
|
-
|
|
402
|
+
|
|
371
403
|
if hasattr(source_obj, field.name):
|
|
372
404
|
# Use centralized field value extraction for consistent FK handling
|
|
373
405
|
value = get_field_value_for_db(source_obj, field.name, source_obj.__class__)
|
|
@@ -13,15 +13,15 @@ django_bulk_hooks/manager.py,sha256=3mFzB0ZzHHeXWdKGObZD_H0NlskHJc8uYBF69KKdAXU,
|
|
|
13
13
|
django_bulk_hooks/models.py,sha256=4Vvi2LiGP0g4j08a5liqBROfsO8Wd_ermBoyjKwfrPU,2512
|
|
14
14
|
django_bulk_hooks/operations/__init__.py,sha256=BtJYjmRhe_sScivLsniDaZmBkm0ZLvcmzXFKL7QY2Xg,550
|
|
15
15
|
django_bulk_hooks/operations/analyzer.py,sha256=wAG8sAG9NwfwNqG9z81VfGR7AANDzRmMGE_o82MWji4,10689
|
|
16
|
-
django_bulk_hooks/operations/bulk_executor.py,sha256=
|
|
16
|
+
django_bulk_hooks/operations/bulk_executor.py,sha256=Y-wkvuV_X-SZmI965JVrrtwbzPZVggUfy8mR1pzP9d0,27048
|
|
17
17
|
django_bulk_hooks/operations/coordinator.py,sha256=iGavJLqe3eYRqFay8cMn6muwyRYzQo-HFGphsS5hL6g,30799
|
|
18
18
|
django_bulk_hooks/operations/field_utils.py,sha256=Tvr5bcZLG8imH-r2S85oui1Cbw6hGv3VtuIMn4OvsU4,2895
|
|
19
|
-
django_bulk_hooks/operations/mti_handler.py,sha256=
|
|
19
|
+
django_bulk_hooks/operations/mti_handler.py,sha256=MMDxuE6jzwkhMwVE-6qJUdp4MNKgAIFPMjwdN_umrqs,23503
|
|
20
20
|
django_bulk_hooks/operations/mti_plans.py,sha256=7STQ2oA2ZT8cEG3-t-6xciRAdf7OeSf0gRLXR_BRG-Q,3363
|
|
21
21
|
django_bulk_hooks/operations/record_classifier.py,sha256=kqML4aO11X9K3SSJ5DUlUukwI172j_Tk12Kr77ee8q8,7065
|
|
22
22
|
django_bulk_hooks/queryset.py,sha256=aQitlbexcVnmeAdc0jtO3hci39p4QEu4srQPEzozy5s,5546
|
|
23
23
|
django_bulk_hooks/registry.py,sha256=uum5jhGI3TPaoiXuA1MdBdu4gbE3rQGGwQ5YDjiMcjk,7949
|
|
24
|
-
django_bulk_hooks-0.2.
|
|
25
|
-
django_bulk_hooks-0.2.
|
|
26
|
-
django_bulk_hooks-0.2.
|
|
27
|
-
django_bulk_hooks-0.2.
|
|
24
|
+
django_bulk_hooks-0.2.57.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
25
|
+
django_bulk_hooks-0.2.57.dist-info/METADATA,sha256=SWZOMPOgiBDCLuSxHX9xTeJMqcd3CgvDZP-JuoY4wRs,9265
|
|
26
|
+
django_bulk_hooks-0.2.57.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
27
|
+
django_bulk_hooks-0.2.57.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|