django-bulk-hooks 0.2.93__tar.gz → 0.2.103__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.
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/PKG-INFO +1 -1
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/dispatcher.py +90 -24
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/bulk_executor.py +23 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/field_utils.py +9 -3
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/mti_handler.py +9 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/pyproject.toml +1 -1
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/LICENSE +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/README.md +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/changeset.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/factory.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/helpers.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/__init__.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/analyzer.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/coordinator.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/mti_plans.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/record_classifier.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/queryset.py +0 -0
- {django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/registry.py +0 -0
|
@@ -101,7 +101,9 @@ class HookDispatcher:
|
|
|
101
101
|
hooks = self.registry.get_hooks(changeset.model_cls, event)
|
|
102
102
|
|
|
103
103
|
logger.debug(f"🧵 DISPATCH: changeset.model_cls={changeset.model_cls.__name__}, event={event}")
|
|
104
|
-
logger.debug(
|
|
104
|
+
logger.debug(
|
|
105
|
+
f"🎣 HOOKS_FOUND: {len(hooks)} hooks for {changeset.model_cls.__name__}.{event}: {[f'{h[0].__name__}.{h[1]}' for h in hooks]}"
|
|
106
|
+
)
|
|
105
107
|
|
|
106
108
|
if not hooks:
|
|
107
109
|
return
|
|
@@ -125,11 +127,11 @@ class HookDispatcher:
|
|
|
125
127
|
sorted_record_ids = tuple(record_ids)
|
|
126
128
|
|
|
127
129
|
# Include changeset model and operation details to make the key more specific
|
|
128
|
-
operation_meta = getattr(changeset,
|
|
129
|
-
operation_type = getattr(changeset,
|
|
130
|
+
operation_meta = getattr(changeset, "operation_meta", {}) or {}
|
|
131
|
+
operation_type = getattr(changeset, "operation_type", "unknown")
|
|
130
132
|
|
|
131
133
|
# Include update_kwargs if present to distinguish different queryset operations
|
|
132
|
-
update_kwargs = operation_meta.get(
|
|
134
|
+
update_kwargs = operation_meta.get("update_kwargs", {})
|
|
133
135
|
if update_kwargs:
|
|
134
136
|
try:
|
|
135
137
|
# Convert to a hashable representation
|
|
@@ -143,7 +145,7 @@ class HookDispatcher:
|
|
|
143
145
|
operation_key = (event, changeset.model_cls.__name__, operation_type, sorted_record_ids, update_kwargs_key)
|
|
144
146
|
|
|
145
147
|
# Track executed hooks to prevent duplicates in MTI inheritance chains
|
|
146
|
-
if not hasattr(self,
|
|
148
|
+
if not hasattr(self, "_executed_hooks"):
|
|
147
149
|
self._executed_hooks = set()
|
|
148
150
|
|
|
149
151
|
# Filter out hooks that have already been executed for this operation
|
|
@@ -224,7 +226,7 @@ class HookDispatcher:
|
|
|
224
226
|
)
|
|
225
227
|
|
|
226
228
|
# Mark that relationships have been preloaded to avoid duplicate condition preloading
|
|
227
|
-
changeset.operation_meta[
|
|
229
|
+
changeset.operation_meta["relationships_preloaded"] = True
|
|
228
230
|
logger.debug(f"🔗 @select_related: Preloaded relationships for {handler_cls.__name__}.{method_name}")
|
|
229
231
|
|
|
230
232
|
except Exception as e:
|
|
@@ -239,9 +241,11 @@ class HookDispatcher:
|
|
|
239
241
|
# NOW condition evaluation is safe - relationships are preloaded
|
|
240
242
|
if condition:
|
|
241
243
|
# Skip per-hook preloading if relationships were already preloaded upfront
|
|
242
|
-
if not changeset.operation_meta.get(
|
|
244
|
+
if not changeset.operation_meta.get("relationships_preloaded", False):
|
|
243
245
|
condition_relationships = self._extract_condition_relationships(condition, changeset.model_cls)
|
|
244
|
-
logger.info(
|
|
246
|
+
logger.info(
|
|
247
|
+
f"🔍 CONDITION: {handler_cls.__name__}.{method_name} has condition, extracted relationships: {condition_relationships}"
|
|
248
|
+
)
|
|
245
249
|
if condition_relationships:
|
|
246
250
|
logger.info(f"🔗 PRELOADING: Preloading condition relationships for {len(changeset.changes)} records")
|
|
247
251
|
self._preload_condition_relationships(changeset, condition_relationships)
|
|
@@ -312,20 +316,20 @@ class HookDispatcher:
|
|
|
312
316
|
relationships = set()
|
|
313
317
|
|
|
314
318
|
# Guard against Mock objects and non-condition objects
|
|
315
|
-
if not hasattr(condition,
|
|
319
|
+
if not hasattr(condition, "check") or hasattr(condition, "_mock_name"):
|
|
316
320
|
return relationships
|
|
317
321
|
|
|
318
322
|
# Handle different condition types
|
|
319
|
-
if hasattr(condition,
|
|
323
|
+
if hasattr(condition, "field"):
|
|
320
324
|
# Extract relationships from field path (e.g., "status__value" -> "status")
|
|
321
325
|
field_path = condition.field
|
|
322
326
|
if isinstance(field_path, str):
|
|
323
|
-
if
|
|
327
|
+
if "__" in field_path:
|
|
324
328
|
# Take the first part before __ (the relationship to preload)
|
|
325
|
-
rel_field = field_path.split(
|
|
329
|
+
rel_field = field_path.split("__")[0]
|
|
326
330
|
|
|
327
331
|
# Normalize FK field names: business_id -> business
|
|
328
|
-
if rel_field.endswith(
|
|
332
|
+
if rel_field.endswith("_id"):
|
|
329
333
|
potential_field_name = rel_field[:-3] # Remove '_id'
|
|
330
334
|
if self._is_relationship_field(model_cls, potential_field_name):
|
|
331
335
|
rel_field = potential_field_name
|
|
@@ -336,7 +340,7 @@ class HookDispatcher:
|
|
|
336
340
|
rel_field = field_path
|
|
337
341
|
|
|
338
342
|
# Normalize FK field names: business_id -> business
|
|
339
|
-
if rel_field.endswith(
|
|
343
|
+
if rel_field.endswith("_id"):
|
|
340
344
|
potential_field_name = rel_field[:-3] # Remove '_id'
|
|
341
345
|
if self._is_relationship_field(model_cls, potential_field_name):
|
|
342
346
|
rel_field = potential_field_name
|
|
@@ -346,12 +350,12 @@ class HookDispatcher:
|
|
|
346
350
|
relationships.add(rel_field)
|
|
347
351
|
|
|
348
352
|
# Handle composite conditions (AndCondition, OrCondition)
|
|
349
|
-
if hasattr(condition,
|
|
353
|
+
if hasattr(condition, "cond1") and hasattr(condition, "cond2"):
|
|
350
354
|
relationships.update(self._extract_condition_relationships(condition.cond1, model_cls))
|
|
351
355
|
relationships.update(self._extract_condition_relationships(condition.cond2, model_cls))
|
|
352
356
|
|
|
353
357
|
# Handle NotCondition
|
|
354
|
-
if hasattr(condition,
|
|
358
|
+
if hasattr(condition, "cond"):
|
|
355
359
|
relationships.update(self._extract_condition_relationships(condition.cond, model_cls))
|
|
356
360
|
|
|
357
361
|
return relationships
|
|
@@ -403,9 +407,40 @@ class HookDispatcher:
|
|
|
403
407
|
preloaded_obj = preloaded[obj.pk]
|
|
404
408
|
for rel in relationship_list:
|
|
405
409
|
if hasattr(preloaded_obj, rel):
|
|
410
|
+
# Preserve FK _id values in __dict__ before setattr (MTI fix)
|
|
411
|
+
id_field_name = f"{rel}_id"
|
|
412
|
+
field_was_in_dict = id_field_name in obj.__dict__
|
|
413
|
+
preserved_id = obj.__dict__.get(id_field_name) if field_was_in_dict else None
|
|
414
|
+
|
|
415
|
+
logger.debug("🔄 PRESERVE_FK_NEW: obj.pk=%s, %s in __dict__=%s, preserved=%s",
|
|
416
|
+
obj.pk, id_field_name, field_was_in_dict, preserved_id)
|
|
417
|
+
|
|
406
418
|
setattr(obj, rel, getattr(preloaded_obj, rel))
|
|
407
419
|
|
|
420
|
+
after_setattr = obj.__dict__.get(id_field_name, "NOT_IN_DICT")
|
|
421
|
+
logger.debug("🔄 AFTER_SETATTR_NEW: obj.pk=%s, %s=%s (was %s)",
|
|
422
|
+
obj.pk, id_field_name, after_setattr, preserved_id)
|
|
423
|
+
|
|
424
|
+
# Restore FK _id if it was in __dict__ (prevents Django descriptor from clearing it)
|
|
425
|
+
# This includes restoring None if that's what was explicitly set
|
|
426
|
+
if field_was_in_dict:
|
|
427
|
+
obj.__dict__[id_field_name] = preserved_id
|
|
428
|
+
# Also clear the relationship from fields_cache to prevent Django from
|
|
429
|
+
# returning the cached object when preserved_id is None
|
|
430
|
+
if preserved_id is None and hasattr(obj, "_state") and hasattr(obj._state, "fields_cache"):
|
|
431
|
+
if rel in obj._state.fields_cache:
|
|
432
|
+
obj._state.fields_cache.pop(rel, None)
|
|
433
|
+
logger.debug("🔄 CLEARED_CACHE: obj.pk=%s, cleared '%s' from fields_cache",
|
|
434
|
+
obj.pk, rel)
|
|
435
|
+
logger.debug("🔄 RESTORED_FK_NEW: obj.pk=%s, %s=%s",
|
|
436
|
+
obj.pk, id_field_name, obj.__dict__.get(id_field_name))
|
|
437
|
+
|
|
408
438
|
# Update old_records with preloaded relationships
|
|
439
|
+
# NOTE: We do NOT preserve/restore FK _id values for old_records because:
|
|
440
|
+
# 1. old_records represent the "before" state for hook conditions
|
|
441
|
+
# 2. They should reflect database values, not in-memory user changes
|
|
442
|
+
# 3. When new_records and old_records share the same object instances,
|
|
443
|
+
# restoring DB values here would overwrite user's in-memory changes
|
|
409
444
|
if changeset.old_records:
|
|
410
445
|
for obj in changeset.old_records:
|
|
411
446
|
if obj.pk and obj.pk in preloaded:
|
|
@@ -414,6 +449,13 @@ class HookDispatcher:
|
|
|
414
449
|
if hasattr(preloaded_obj, rel):
|
|
415
450
|
setattr(obj, rel, getattr(preloaded_obj, rel))
|
|
416
451
|
|
|
452
|
+
# Log final state after preloading
|
|
453
|
+
if changeset.new_records:
|
|
454
|
+
for obj in changeset.new_records:
|
|
455
|
+
if "business_id" in obj.__dict__:
|
|
456
|
+
logger.debug("🔄 FINAL_STATE_NEW: obj.pk=%s, business_id=%s",
|
|
457
|
+
obj.pk, obj.__dict__.get("business_id"))
|
|
458
|
+
|
|
417
459
|
# Handle unsaved new_records by preloading their FK targets (bulk query to avoid N+1)
|
|
418
460
|
if changeset.new_records:
|
|
419
461
|
# Collect FK IDs for each relationship from unsaved records
|
|
@@ -422,8 +464,8 @@ class HookDispatcher:
|
|
|
422
464
|
for obj in changeset.new_records:
|
|
423
465
|
if obj.pk is None: # Unsaved object
|
|
424
466
|
for rel in relationship_list:
|
|
425
|
-
if hasattr(obj, f
|
|
426
|
-
rel_id = getattr(obj, f
|
|
467
|
+
if hasattr(obj, f"{rel}_id"):
|
|
468
|
+
rel_id = getattr(obj, f"{rel}_id")
|
|
427
469
|
if rel_id:
|
|
428
470
|
field_ids_map[rel].add(rel_id)
|
|
429
471
|
|
|
@@ -433,7 +475,7 @@ class HookDispatcher:
|
|
|
433
475
|
if not ids:
|
|
434
476
|
continue
|
|
435
477
|
try:
|
|
436
|
-
rel_model = getattr(changeset.model_cls._meta.get_field(rel).remote_field,
|
|
478
|
+
rel_model = getattr(changeset.model_cls._meta.get_field(rel).remote_field, "model")
|
|
437
479
|
field_objects_map[rel] = rel_model.objects.in_bulk(ids)
|
|
438
480
|
except Exception:
|
|
439
481
|
field_objects_map[rel] = {}
|
|
@@ -442,12 +484,26 @@ class HookDispatcher:
|
|
|
442
484
|
for obj in changeset.new_records:
|
|
443
485
|
if obj.pk is None: # Unsaved object
|
|
444
486
|
for rel in relationship_list:
|
|
445
|
-
rel_id = getattr(obj, f
|
|
487
|
+
rel_id = getattr(obj, f"{rel}_id", None)
|
|
446
488
|
if rel_id and rel in field_objects_map:
|
|
447
489
|
rel_obj = field_objects_map[rel].get(rel_id)
|
|
448
490
|
if rel_obj:
|
|
491
|
+
# Preserve FK _id value before setattr (MTI/upsert fix)
|
|
492
|
+
id_field_name = f"{rel}_id"
|
|
493
|
+
field_was_in_dict = id_field_name in obj.__dict__
|
|
494
|
+
preserved_id = obj.__dict__.get(id_field_name) if field_was_in_dict else None
|
|
495
|
+
|
|
496
|
+
logger.debug("🔄 PRESERVE_FK_UNSAVED: obj.pk=%s, %s in __dict__=%s, preserved=%s",
|
|
497
|
+
obj.pk, id_field_name, field_was_in_dict, preserved_id)
|
|
498
|
+
|
|
449
499
|
setattr(obj, rel, rel_obj)
|
|
450
500
|
|
|
501
|
+
# Restore FK _id if it was in __dict__ (prevents Django descriptor from clearing it)
|
|
502
|
+
if field_was_in_dict and preserved_id is not None:
|
|
503
|
+
obj.__dict__[id_field_name] = preserved_id
|
|
504
|
+
logger.debug("🔄 RESTORED_FK_UNSAVED: obj.pk=%s, %s=%s",
|
|
505
|
+
obj.pk, id_field_name, obj.__dict__.get(id_field_name))
|
|
506
|
+
|
|
451
507
|
def _preload_select_related_for_before_create(self, changeset, select_related_fields):
|
|
452
508
|
"""
|
|
453
509
|
Explicit bulk preloading for @select_related on BEFORE_CREATE hooks.
|
|
@@ -462,7 +518,7 @@ class HookDispatcher:
|
|
|
462
518
|
select_related_fields: List of field names to preload (e.g., ['financial_account'])
|
|
463
519
|
"""
|
|
464
520
|
# Ensure select_related_fields is actually iterable (not a Mock in tests)
|
|
465
|
-
if not select_related_fields or not changeset.new_records or not hasattr(select_related_fields,
|
|
521
|
+
if not select_related_fields or not changeset.new_records or not hasattr(select_related_fields, "__iter__"):
|
|
466
522
|
return
|
|
467
523
|
|
|
468
524
|
logger.info(f"🔗 BULK PRELOAD: Preloading {select_related_fields} for {len(changeset.new_records)} unsaved records")
|
|
@@ -472,7 +528,7 @@ class HookDispatcher:
|
|
|
472
528
|
|
|
473
529
|
for record in changeset.new_records:
|
|
474
530
|
for field in select_related_fields:
|
|
475
|
-
fk_id = getattr(record, f
|
|
531
|
+
fk_id = getattr(record, f"{field}_id", None)
|
|
476
532
|
if fk_id is not None:
|
|
477
533
|
field_ids_map[field].add(fk_id)
|
|
478
534
|
|
|
@@ -501,13 +557,23 @@ class HookDispatcher:
|
|
|
501
557
|
# Attach relationships to each record
|
|
502
558
|
for record in changeset.new_records:
|
|
503
559
|
for field in select_related_fields:
|
|
504
|
-
fk_id = getattr(record, f
|
|
560
|
+
fk_id = getattr(record, f"{field}_id", None)
|
|
505
561
|
if fk_id is not None and field in field_objects_map:
|
|
506
562
|
related_obj = field_objects_map[field].get(fk_id)
|
|
507
563
|
if related_obj is not None:
|
|
564
|
+
# Preserve FK _id value before setattr (MTI/upsert fix)
|
|
565
|
+
id_field_name = f"{field}_id"
|
|
566
|
+
field_was_in_dict = id_field_name in record.__dict__
|
|
567
|
+
preserved_id = record.__dict__.get(id_field_name) if field_was_in_dict else None
|
|
568
|
+
|
|
508
569
|
setattr(record, field, related_obj)
|
|
570
|
+
|
|
571
|
+
# Restore FK _id if it was in __dict__ (prevents Django descriptor from clearing it)
|
|
572
|
+
if field_was_in_dict and preserved_id is not None:
|
|
573
|
+
record.__dict__[id_field_name] = preserved_id
|
|
574
|
+
|
|
509
575
|
# Also cache in Django's fields_cache for consistency
|
|
510
|
-
if hasattr(record,
|
|
576
|
+
if hasattr(record, "_state") and hasattr(record._state, "fields_cache"):
|
|
511
577
|
record._state.fields_cache[field] = related_obj
|
|
512
578
|
|
|
513
579
|
logger.info(f"🔗 BULK PRELOAD: Completed relationship attachment for {len(changeset.new_records)} records")
|
{django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/bulk_executor.py
RENAMED
|
@@ -150,6 +150,13 @@ class BulkExecutor:
|
|
|
150
150
|
if not objs:
|
|
151
151
|
return 0
|
|
152
152
|
|
|
153
|
+
# Debug: Check FK values at bulk_update entry point
|
|
154
|
+
for obj in objs:
|
|
155
|
+
logger.debug("🚀 BULK_UPDATE_ENTRY: obj.pk=%s, business_id in __dict__=%s, value=%s",
|
|
156
|
+
getattr(obj, 'pk', 'None'),
|
|
157
|
+
'business_id' in obj.__dict__,
|
|
158
|
+
obj.__dict__.get('business_id', 'NOT_IN_DICT'))
|
|
159
|
+
|
|
153
160
|
# Ensure auto_now fields are included
|
|
154
161
|
fields = self._add_auto_now_fields(fields, objs)
|
|
155
162
|
|
|
@@ -674,6 +681,14 @@ class BulkExecutor:
|
|
|
674
681
|
|
|
675
682
|
logger.debug(f"Building CASE statements for {field_group.model_class.__name__} with {len(field_group.fields)} fields")
|
|
676
683
|
|
|
684
|
+
# Debug: Check if business_id is still in __dict__ before field extraction
|
|
685
|
+
for obj in objs:
|
|
686
|
+
if 'business_id' in obj.__dict__ or 'business' in field_group.fields:
|
|
687
|
+
logger.debug("🏗️ CASE_BUILD_START: obj.pk=%s, business_id in __dict__=%s, value=%s",
|
|
688
|
+
getattr(obj, 'pk', 'None'),
|
|
689
|
+
'business_id' in obj.__dict__,
|
|
690
|
+
obj.__dict__.get('business_id', 'NOT_IN_DICT'))
|
|
691
|
+
|
|
677
692
|
for field_name in field_group.fields:
|
|
678
693
|
case_stmt = self._build_field_case_statement(field_name, field_group, root_pks, objs)
|
|
679
694
|
if case_stmt:
|
|
@@ -699,6 +714,13 @@ class BulkExecutor:
|
|
|
699
714
|
|
|
700
715
|
# Get and convert field value
|
|
701
716
|
value = get_field_value_for_db(obj, field_name, field_group.model_class)
|
|
717
|
+
|
|
718
|
+
# Debug: Track business field extraction
|
|
719
|
+
if field_name == 'business':
|
|
720
|
+
logger.debug("🔧 CASE_FIELD_VALUE: obj.pk=%s, field='%s', raw_value=%s, business_id in __dict__=%s",
|
|
721
|
+
obj_pk, field_name, value,
|
|
722
|
+
'business_id' in obj.__dict__ if hasattr(obj, '__dict__') else 'N/A')
|
|
723
|
+
|
|
702
724
|
value = field.to_python(value)
|
|
703
725
|
|
|
704
726
|
# Create WHEN with type casting
|
|
@@ -722,6 +744,7 @@ class BulkExecutor:
|
|
|
722
744
|
) -> int:
|
|
723
745
|
"""Execute the actual update query."""
|
|
724
746
|
logger.debug(f"Executing update for {field_group.model_class.__name__} with {len(case_statements)} fields")
|
|
747
|
+
logger.debug(f"📝 UPDATE_FIELDS: {field_group.model_class.__name__} updating fields: {list(case_statements.keys())}")
|
|
725
748
|
|
|
726
749
|
try:
|
|
727
750
|
query_qs = base_qs.filter(**{f"{field_group.filter_field}__in": root_pks})
|
{django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/field_utils.py
RENAMED
|
@@ -70,9 +70,15 @@ def _extract_fk_value(obj, field_name, field, model_cls):
|
|
|
70
70
|
attname, field_was_explicitly_set, list(obj.__dict__.keys()))
|
|
71
71
|
|
|
72
72
|
# Try direct access first
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
73
|
+
# For MTI scenarios, try __dict__ first to avoid Django descriptor database lookups
|
|
74
|
+
if obj.__class__ != model_cls and attname in obj.__dict__:
|
|
75
|
+
value = obj.__dict__[attname]
|
|
76
|
+
logger.debug("🔍 FK_DIRECT_ACCESS_DICT: obj.__dict__['%s'] = %s (type: %s) [MTI bypass]",
|
|
77
|
+
attname, value, type(value).__name__ if value is not None else 'None')
|
|
78
|
+
else:
|
|
79
|
+
value = getattr(obj, attname, None)
|
|
80
|
+
logger.debug("🔍 FK_DIRECT_ACCESS: getattr(obj, '%s') = %s (type: %s)",
|
|
81
|
+
attname, value, type(value).__name__ if value is not None else 'None')
|
|
76
82
|
|
|
77
83
|
# For MTI scenarios where parent field access fails on child instance
|
|
78
84
|
# OR when the _id field is None but the relationship field is set (common in MTI)
|
{django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/mti_handler.py
RENAMED
|
@@ -165,6 +165,9 @@ class MTIHandler:
|
|
|
165
165
|
batch_size = batch_size or len(objs)
|
|
166
166
|
existing_record_ids = existing_record_ids or set()
|
|
167
167
|
existing_pks_map = existing_pks_map or {}
|
|
168
|
+
|
|
169
|
+
logger.debug("🔧 MTI_CREATE_PLAN: model=%s, update_conflicts=%s, unique_fields=%s, update_fields=%s",
|
|
170
|
+
self.model_cls.__name__, update_conflicts, unique_fields, update_fields)
|
|
168
171
|
|
|
169
172
|
# Set PKs on existing objects for proper updates
|
|
170
173
|
self._set_existing_pks(objs, existing_pks_map)
|
|
@@ -410,7 +413,13 @@ class MTIHandler:
|
|
|
410
413
|
update_fields: Optional[List[str]],
|
|
411
414
|
) -> Dict[str, any]:
|
|
412
415
|
"""Build upsert config for levels with matching unique constraints."""
|
|
416
|
+
logger.debug("🔧 UPSERT_CONFIG: model=%s, incoming_update_fields=%s, local_fields=%s",
|
|
417
|
+
model_class.__name__, update_fields, list(model_fields_by_name.keys()))
|
|
418
|
+
|
|
413
419
|
filtered_updates = [uf for uf in (update_fields or []) if uf in model_fields_by_name]
|
|
420
|
+
|
|
421
|
+
logger.debug("🔧 UPSERT_FILTERED: model=%s, filtered_update_fields=%s",
|
|
422
|
+
model_class.__name__, filtered_updates)
|
|
414
423
|
|
|
415
424
|
# Add auto_now fields (critical for timestamp updates)
|
|
416
425
|
auto_now_fields = self._get_auto_now_fields(model_class, model_fields_by_name)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "django-bulk-hooks"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.103"
|
|
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
|
{django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/__init__.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/analyzer.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/coordinator.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.93 → django_bulk_hooks-0.2.103}/django_bulk_hooks/operations/mti_plans.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|