django-bulk-hooks 0.2.56__tar.gz → 0.2.58__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.2.56 → django_bulk_hooks-0.2.58}/PKG-INFO +1 -1
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/bulk_executor.py +52 -2
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/mti_handler.py +45 -3
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/pyproject.toml +1 -1
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/LICENSE +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/README.md +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/changeset.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/dispatcher.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/factory.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/helpers.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/__init__.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/analyzer.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/coordinator.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/field_utils.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/mti_plans.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/record_classifier.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/registry.py +0 -0
{django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/bulk_executor.py
RENAMED
|
@@ -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):
|
{django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/mti_handler.py
RENAMED
|
@@ -252,7 +252,7 @@ class MTIHandler:
|
|
|
252
252
|
|
|
253
253
|
parent_levels = []
|
|
254
254
|
parent_instances_map = {} # Maps obj id() -> {model_class: parent_instance}
|
|
255
|
-
|
|
255
|
+
|
|
256
256
|
# Set defaults
|
|
257
257
|
if existing_record_ids is None:
|
|
258
258
|
existing_record_ids = set()
|
|
@@ -306,6 +306,14 @@ class MTIHandler:
|
|
|
306
306
|
if not filtered_updates and normalized_unique:
|
|
307
307
|
filtered_updates = [normalized_unique[0]]
|
|
308
308
|
|
|
309
|
+
# CRITICAL FIX: Always include auto_now fields in updates to ensure timestamps are updated.
|
|
310
|
+
# During MTI upsert, parent tables need auto_now fields updated even when only child fields change.
|
|
311
|
+
# This ensures parent-level timestamps (e.g., updated_at) refresh correctly on upsert.
|
|
312
|
+
auto_now_fields = self._get_auto_now_fields_for_model(model_class, model_fields_by_name)
|
|
313
|
+
if auto_now_fields:
|
|
314
|
+
# Convert to set to avoid duplicates, then back to list for consistency
|
|
315
|
+
filtered_updates = list(set(filtered_updates) | set(auto_now_fields))
|
|
316
|
+
|
|
309
317
|
# Only enable upsert if we have fields to update (real or dummy)
|
|
310
318
|
if filtered_updates:
|
|
311
319
|
level_update_conflicts = True
|
|
@@ -327,10 +335,21 @@ class MTIHandler:
|
|
|
327
335
|
level_unique_fields = [pk_field.name]
|
|
328
336
|
# Use a safe update field - pick the first available non-PK field
|
|
329
337
|
# or use the PK itself as a dummy (updating to itself is a no-op)
|
|
330
|
-
available_fields = [
|
|
331
|
-
|
|
338
|
+
available_fields = [
|
|
339
|
+
f.name
|
|
340
|
+
for f in model_class._meta.local_fields
|
|
341
|
+
if not isinstance(f, AutoField) and f.name in model_fields_by_name
|
|
342
|
+
]
|
|
332
343
|
level_update_fields = available_fields[:1] if available_fields else [pk_field.name]
|
|
333
344
|
|
|
345
|
+
# CRITICAL FIX: Include auto_now fields in update_fields to ensure timestamps are updated.
|
|
346
|
+
# During MTI upsert, parent tables need auto_now fields updated even when using dummy fields.
|
|
347
|
+
# This ensures parent-level timestamps (e.g., updated_at) refresh correctly on upsert.
|
|
348
|
+
auto_now_fields = self._get_auto_now_fields_for_model(model_class, model_fields_by_name)
|
|
349
|
+
if auto_now_fields:
|
|
350
|
+
# Convert to set to avoid duplicates, then back to list for consistency
|
|
351
|
+
level_update_fields = list(set(level_update_fields) | set(auto_now_fields))
|
|
352
|
+
|
|
334
353
|
# Create parent level
|
|
335
354
|
parent_level = ParentLevel(
|
|
336
355
|
model_class=model_class,
|
|
@@ -344,6 +363,29 @@ class MTIHandler:
|
|
|
344
363
|
|
|
345
364
|
return parent_levels
|
|
346
365
|
|
|
366
|
+
def _get_auto_now_fields_for_model(self, model_class, model_fields_by_name):
|
|
367
|
+
"""
|
|
368
|
+
Get auto_now (not auto_now_add) fields for a specific model.
|
|
369
|
+
|
|
370
|
+
Only includes fields that exist in model_fields_by_name to ensure
|
|
371
|
+
they're valid local fields for this model level.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
model_class: Model class to get fields for
|
|
375
|
+
model_fields_by_name: Dict of valid field names for this model level
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
List of auto_now field names (excluding auto_now_add)
|
|
379
|
+
"""
|
|
380
|
+
auto_now_fields = []
|
|
381
|
+
for field in model_class._meta.local_fields:
|
|
382
|
+
# Only include auto_now (not auto_now_add) since auto_now_add should only be set on creation
|
|
383
|
+
if getattr(field, "auto_now", False) and not getattr(field, "auto_now_add", False):
|
|
384
|
+
# Double-check field exists in model_fields_by_name for safety
|
|
385
|
+
if field.name in model_fields_by_name:
|
|
386
|
+
auto_now_fields.append(field.name)
|
|
387
|
+
return auto_now_fields
|
|
388
|
+
|
|
347
389
|
def _has_matching_constraint(self, model_class, normalized_unique):
|
|
348
390
|
"""Check if model has a unique constraint matching the given fields."""
|
|
349
391
|
try:
|
|
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
|
|
File without changes
|
{django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/__init__.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/analyzer.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/coordinator.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/field_utils.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.56 → django_bulk_hooks-0.2.58}/django_bulk_hooks/operations/mti_plans.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|