django-bulk-hooks 0.2.43__tar.gz → 0.2.45__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.43 → django_bulk_hooks-0.2.45}/PKG-INFO +1 -1
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/operations/bulk_executor.py +55 -7
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/operations/coordinator.py +173 -41
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/registry.py +1 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/pyproject.toml +1 -1
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/LICENSE +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/README.md +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/__init__.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/changeset.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/conditions.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/constants.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/context.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/decorators.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/dispatcher.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/enums.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/factory.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/handler.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/helpers.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/manager.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/models.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/operations/__init__.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/operations/analyzer.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/operations/mti_handler.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/operations/mti_plans.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/operations/record_classifier.py +0 -0
- {django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/queryset.py +0 -0
{django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/operations/bulk_executor.py
RENAMED
|
@@ -47,6 +47,8 @@ class BulkExecutor:
|
|
|
47
47
|
update_conflicts=False,
|
|
48
48
|
update_fields=None,
|
|
49
49
|
unique_fields=None,
|
|
50
|
+
existing_record_ids=None,
|
|
51
|
+
existing_pks_map=None,
|
|
50
52
|
**kwargs,
|
|
51
53
|
):
|
|
52
54
|
"""
|
|
@@ -74,11 +76,12 @@ class BulkExecutor:
|
|
|
74
76
|
if self.mti_handler.is_mti_model():
|
|
75
77
|
logger.info(f"Detected MTI model {self.model_cls.__name__}, using MTI bulk create")
|
|
76
78
|
|
|
77
|
-
#
|
|
78
|
-
existing_record_ids
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
# Use pre-classified records if provided, otherwise classify now
|
|
80
|
+
if existing_record_ids is None or existing_pks_map is None:
|
|
81
|
+
existing_record_ids = set()
|
|
82
|
+
existing_pks_map = {}
|
|
83
|
+
if update_conflicts and unique_fields:
|
|
84
|
+
existing_record_ids, existing_pks_map = self.record_classifier.classify_for_upsert(objs, unique_fields)
|
|
82
85
|
|
|
83
86
|
# Build execution plan with classification results
|
|
84
87
|
plan = self.mti_handler.build_create_plan(
|
|
@@ -91,10 +94,16 @@ class BulkExecutor:
|
|
|
91
94
|
existing_pks_map=existing_pks_map,
|
|
92
95
|
)
|
|
93
96
|
# Execute the plan
|
|
94
|
-
|
|
97
|
+
result = self._execute_mti_create_plan(plan)
|
|
98
|
+
|
|
99
|
+
# Tag objects with upsert metadata for hook dispatching
|
|
100
|
+
if update_conflicts and unique_fields:
|
|
101
|
+
self._tag_upsert_metadata(result, existing_record_ids)
|
|
102
|
+
|
|
103
|
+
return result
|
|
95
104
|
|
|
96
105
|
# Non-MTI model - use Django's native bulk_create
|
|
97
|
-
|
|
106
|
+
result = self._execute_bulk_create(
|
|
98
107
|
objs,
|
|
99
108
|
batch_size,
|
|
100
109
|
ignore_conflicts,
|
|
@@ -103,6 +112,15 @@ class BulkExecutor:
|
|
|
103
112
|
unique_fields,
|
|
104
113
|
**kwargs,
|
|
105
114
|
)
|
|
115
|
+
|
|
116
|
+
# Tag objects with upsert metadata for hook dispatching
|
|
117
|
+
if update_conflicts and unique_fields:
|
|
118
|
+
# Use pre-classified results if available, otherwise classify now
|
|
119
|
+
if existing_record_ids is None:
|
|
120
|
+
existing_record_ids, _ = self.record_classifier.classify_for_upsert(objs, unique_fields)
|
|
121
|
+
self._tag_upsert_metadata(result, existing_record_ids)
|
|
122
|
+
|
|
123
|
+
return result
|
|
106
124
|
|
|
107
125
|
def _execute_bulk_create(
|
|
108
126
|
self,
|
|
@@ -510,3 +528,33 @@ class BulkExecutor:
|
|
|
510
528
|
from django.db.models import QuerySet
|
|
511
529
|
|
|
512
530
|
return QuerySet.delete(self.queryset)
|
|
531
|
+
|
|
532
|
+
def _tag_upsert_metadata(self, result_objects, existing_record_ids):
|
|
533
|
+
"""
|
|
534
|
+
Tag objects with metadata indicating whether they were created or updated.
|
|
535
|
+
|
|
536
|
+
This metadata is used by the coordinator to determine which hooks to fire.
|
|
537
|
+
The metadata is temporary and will be cleaned up after hook execution.
|
|
538
|
+
|
|
539
|
+
Args:
|
|
540
|
+
result_objects: List of objects returned from bulk operation
|
|
541
|
+
existing_record_ids: Set of id() for objects that existed before the operation
|
|
542
|
+
"""
|
|
543
|
+
created_count = 0
|
|
544
|
+
updated_count = 0
|
|
545
|
+
|
|
546
|
+
for obj in result_objects:
|
|
547
|
+
# Tag with metadata for hook dispatching
|
|
548
|
+
was_created = id(obj) not in existing_record_ids
|
|
549
|
+
obj._bulk_hooks_was_created = was_created
|
|
550
|
+
obj._bulk_hooks_upsert_metadata = True
|
|
551
|
+
|
|
552
|
+
if was_created:
|
|
553
|
+
created_count += 1
|
|
554
|
+
else:
|
|
555
|
+
updated_count += 1
|
|
556
|
+
|
|
557
|
+
logger.info(
|
|
558
|
+
f"Tagged upsert metadata: {created_count} created, {updated_count} updated "
|
|
559
|
+
f"(total={len(result_objects)}, existing_ids={len(existing_record_ids)})"
|
|
560
|
+
)
|
{django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/operations/coordinator.py
RENAMED
|
@@ -29,7 +29,6 @@ class BulkOperationCoordinator:
|
|
|
29
29
|
Services are created lazily and cached.
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
|
|
33
32
|
def __init__(self, queryset):
|
|
34
33
|
"""
|
|
35
34
|
Initialize coordinator for a queryset.
|
|
@@ -133,6 +132,15 @@ class BulkOperationCoordinator:
|
|
|
133
132
|
# Validate
|
|
134
133
|
self.analyzer.validate_for_create(objs)
|
|
135
134
|
|
|
135
|
+
# For upsert operations, classify records upfront
|
|
136
|
+
existing_record_ids = set()
|
|
137
|
+
existing_pks_map = {}
|
|
138
|
+
if update_conflicts and unique_fields:
|
|
139
|
+
existing_record_ids, existing_pks_map = self.record_classifier.classify_for_upsert(objs, unique_fields)
|
|
140
|
+
logger.info(f"Upsert operation: {len(existing_record_ids)} existing, {len(objs) - len(existing_record_ids)} new records")
|
|
141
|
+
logger.debug(f"Existing record IDs: {existing_record_ids}")
|
|
142
|
+
logger.debug(f"Existing PKs map: {existing_pks_map}")
|
|
143
|
+
|
|
136
144
|
# Build initial changeset
|
|
137
145
|
changeset = build_changeset_for_create(
|
|
138
146
|
self.model_cls,
|
|
@@ -153,6 +161,8 @@ class BulkOperationCoordinator:
|
|
|
153
161
|
update_conflicts=update_conflicts,
|
|
154
162
|
update_fields=update_fields,
|
|
155
163
|
unique_fields=unique_fields,
|
|
164
|
+
existing_record_ids=existing_record_ids,
|
|
165
|
+
existing_pks_map=existing_pks_map,
|
|
156
166
|
)
|
|
157
167
|
|
|
158
168
|
return self._execute_with_mti_hooks(
|
|
@@ -222,14 +232,17 @@ class BulkOperationCoordinator:
|
|
|
222
232
|
|
|
223
233
|
@transaction.atomic
|
|
224
234
|
def update_queryset(
|
|
225
|
-
self,
|
|
235
|
+
self,
|
|
236
|
+
update_kwargs,
|
|
237
|
+
bypass_hooks=False,
|
|
238
|
+
bypass_validation=False,
|
|
226
239
|
):
|
|
227
240
|
"""
|
|
228
241
|
Execute queryset.update() with full hook support.
|
|
229
|
-
|
|
242
|
+
|
|
230
243
|
ARCHITECTURE & PERFORMANCE TRADE-OFFS
|
|
231
244
|
======================================
|
|
232
|
-
|
|
245
|
+
|
|
233
246
|
To support hooks with queryset.update(), we must:
|
|
234
247
|
1. Fetch old state (SELECT all matching rows)
|
|
235
248
|
2. Execute database update (UPDATE in SQL)
|
|
@@ -238,29 +251,29 @@ class BulkOperationCoordinator:
|
|
|
238
251
|
5. Run BEFORE_UPDATE hooks (CAN modify instances)
|
|
239
252
|
6. Persist BEFORE_UPDATE modifications (bulk_update)
|
|
240
253
|
7. Run AFTER_UPDATE hooks (read-only side effects)
|
|
241
|
-
|
|
254
|
+
|
|
242
255
|
Performance Cost:
|
|
243
256
|
- 2 SELECT queries (before/after)
|
|
244
257
|
- 1 UPDATE query (actual update)
|
|
245
258
|
- 1 bulk_update (if hooks modify data)
|
|
246
|
-
|
|
259
|
+
|
|
247
260
|
Trade-off: Hooks require loading data into Python. If you need
|
|
248
261
|
maximum performance and don't need hooks, use bypass_hooks=True.
|
|
249
|
-
|
|
262
|
+
|
|
250
263
|
Hook Semantics:
|
|
251
264
|
- BEFORE_UPDATE hooks run after the DB update and CAN modify instances
|
|
252
265
|
- Modifications are auto-persisted (framework handles complexity)
|
|
253
266
|
- AFTER_UPDATE hooks run after BEFORE_UPDATE and are read-only
|
|
254
267
|
- This enables cascade logic and computed fields based on DB values
|
|
255
268
|
- User expectation: BEFORE_UPDATE hooks can modify data
|
|
256
|
-
|
|
269
|
+
|
|
257
270
|
Why this approach works well:
|
|
258
271
|
- Allows hooks to see Subquery/F() computed values
|
|
259
272
|
- Enables HasChanged conditions on complex expressions
|
|
260
273
|
- Maintains SQL performance (Subquery stays in database)
|
|
261
274
|
- Meets user expectations: BEFORE_UPDATE can modify instances
|
|
262
275
|
- Clean separation: BEFORE for modifications, AFTER for side effects
|
|
263
|
-
|
|
276
|
+
|
|
264
277
|
For true "prevent write" semantics, intercept at a higher level
|
|
265
278
|
or use bulk_update() directly (which has true before semantics).
|
|
266
279
|
"""
|
|
@@ -277,19 +290,21 @@ class BulkOperationCoordinator:
|
|
|
277
290
|
)
|
|
278
291
|
|
|
279
292
|
def _execute_queryset_update_with_hooks(
|
|
280
|
-
self,
|
|
293
|
+
self,
|
|
294
|
+
update_kwargs,
|
|
295
|
+
bypass_validation=False,
|
|
281
296
|
):
|
|
282
297
|
"""
|
|
283
298
|
Execute queryset update with full hook lifecycle support.
|
|
284
|
-
|
|
299
|
+
|
|
285
300
|
This method implements the fetch-update-fetch pattern required
|
|
286
301
|
to support hooks with queryset.update(). BEFORE_UPDATE hooks can
|
|
287
302
|
modify instances and modifications are auto-persisted.
|
|
288
|
-
|
|
303
|
+
|
|
289
304
|
Args:
|
|
290
305
|
update_kwargs: Dict of fields to update
|
|
291
306
|
bypass_validation: Skip validation hooks if True
|
|
292
|
-
|
|
307
|
+
|
|
293
308
|
Returns:
|
|
294
309
|
Number of rows updated
|
|
295
310
|
"""
|
|
@@ -373,11 +388,11 @@ class BulkOperationCoordinator:
|
|
|
373
388
|
def _run_before_update_hooks_with_tracking(self, instances, models_in_chain, changeset):
|
|
374
389
|
"""
|
|
375
390
|
Run BEFORE_UPDATE hooks and detect modifications.
|
|
376
|
-
|
|
391
|
+
|
|
377
392
|
This is what users expect - BEFORE_UPDATE hooks can modify instances
|
|
378
393
|
and those modifications will be automatically persisted. The framework
|
|
379
394
|
handles the complexity internally.
|
|
380
|
-
|
|
395
|
+
|
|
381
396
|
Returns:
|
|
382
397
|
Set of field names that were modified by hooks
|
|
383
398
|
"""
|
|
@@ -399,10 +414,10 @@ class BulkOperationCoordinator:
|
|
|
399
414
|
def _snapshot_instance_state(self, instances):
|
|
400
415
|
"""
|
|
401
416
|
Create a snapshot of current instance field values.
|
|
402
|
-
|
|
417
|
+
|
|
403
418
|
Args:
|
|
404
419
|
instances: List of model instances
|
|
405
|
-
|
|
420
|
+
|
|
406
421
|
Returns:
|
|
407
422
|
Dict mapping pk -> {field_name: value}
|
|
408
423
|
"""
|
|
@@ -432,11 +447,11 @@ class BulkOperationCoordinator:
|
|
|
432
447
|
def _detect_modifications(self, instances, pre_hook_state):
|
|
433
448
|
"""
|
|
434
449
|
Detect which fields were modified by comparing to snapshot.
|
|
435
|
-
|
|
450
|
+
|
|
436
451
|
Args:
|
|
437
452
|
instances: List of model instances
|
|
438
453
|
pre_hook_state: Previous state snapshot from _snapshot_instance_state
|
|
439
|
-
|
|
454
|
+
|
|
440
455
|
Returns:
|
|
441
456
|
Set of field names that were modified
|
|
442
457
|
"""
|
|
@@ -463,16 +478,15 @@ class BulkOperationCoordinator:
|
|
|
463
478
|
def _persist_hook_modifications(self, instances, modified_fields):
|
|
464
479
|
"""
|
|
465
480
|
Persist modifications made by hooks using bulk_update.
|
|
466
|
-
|
|
481
|
+
|
|
467
482
|
This creates a "cascade" effect similar to Salesforce workflows.
|
|
468
|
-
|
|
483
|
+
|
|
469
484
|
Args:
|
|
470
485
|
instances: List of modified instances
|
|
471
486
|
modified_fields: Set of field names that were modified
|
|
472
487
|
"""
|
|
473
488
|
logger.info(
|
|
474
|
-
f"Hooks modified {len(modified_fields)} field(s): "
|
|
475
|
-
f"{', '.join(sorted(modified_fields))}",
|
|
489
|
+
f"Hooks modified {len(modified_fields)} field(s): {', '.join(sorted(modified_fields))}",
|
|
476
490
|
)
|
|
477
491
|
logger.info("Auto-persisting modifications via bulk_update")
|
|
478
492
|
|
|
@@ -555,14 +569,14 @@ class BulkOperationCoordinator:
|
|
|
555
569
|
def _build_changeset_for_model(self, original_changeset, target_model_cls):
|
|
556
570
|
"""
|
|
557
571
|
Build a changeset for a specific model in the MTI inheritance chain.
|
|
558
|
-
|
|
572
|
+
|
|
559
573
|
This allows parent model hooks to receive the same instances but with
|
|
560
574
|
the correct model_cls for hook registration matching.
|
|
561
|
-
|
|
575
|
+
|
|
562
576
|
Args:
|
|
563
577
|
original_changeset: The original changeset (for child model)
|
|
564
578
|
target_model_cls: The model class to build changeset for (parent model)
|
|
565
|
-
|
|
579
|
+
|
|
566
580
|
Returns:
|
|
567
581
|
ChangeSet for the target model
|
|
568
582
|
"""
|
|
@@ -586,18 +600,18 @@ class BulkOperationCoordinator:
|
|
|
586
600
|
):
|
|
587
601
|
"""
|
|
588
602
|
Execute operation with hooks for entire MTI inheritance chain.
|
|
589
|
-
|
|
603
|
+
|
|
590
604
|
This method dispatches hooks for both child and parent models when
|
|
591
605
|
dealing with MTI models, ensuring parent model hooks fire when
|
|
592
606
|
child instances are created/updated/deleted.
|
|
593
|
-
|
|
607
|
+
|
|
594
608
|
Args:
|
|
595
609
|
changeset: ChangeSet for the child model
|
|
596
610
|
operation: Callable that performs the actual DB operation
|
|
597
611
|
event_prefix: 'create', 'update', or 'delete'
|
|
598
612
|
bypass_hooks: Skip all hooks if True
|
|
599
613
|
bypass_validation: Skip validation hooks if True
|
|
600
|
-
|
|
614
|
+
|
|
601
615
|
Returns:
|
|
602
616
|
Result of operation
|
|
603
617
|
"""
|
|
@@ -627,13 +641,25 @@ class BulkOperationCoordinator:
|
|
|
627
641
|
# AFTER phase - for all models in chain
|
|
628
642
|
# Use result if operation returns modified data (for create operations)
|
|
629
643
|
if result and isinstance(result, list) and event_prefix == "create":
|
|
630
|
-
#
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
644
|
+
# Check if this was an upsert operation
|
|
645
|
+
is_upsert = self._is_upsert_operation(result)
|
|
646
|
+
if is_upsert:
|
|
647
|
+
# Split hooks for upsert: after_create for created, after_update for updated
|
|
648
|
+
self._dispatch_upsert_after_hooks(result, models_in_chain)
|
|
649
|
+
else:
|
|
650
|
+
# Normal create operation
|
|
651
|
+
from django_bulk_hooks.helpers import build_changeset_for_create
|
|
652
|
+
|
|
653
|
+
changeset = build_changeset_for_create(changeset.model_cls, result)
|
|
654
|
+
|
|
655
|
+
for model_cls in models_in_chain:
|
|
656
|
+
model_changeset = self._build_changeset_for_model(changeset, model_cls)
|
|
657
|
+
self.dispatcher.dispatch(model_changeset, f"after_{event_prefix}", bypass_hooks=False)
|
|
658
|
+
else:
|
|
659
|
+
# Non-create operations (update, delete)
|
|
660
|
+
for model_cls in models_in_chain:
|
|
661
|
+
model_changeset = self._build_changeset_for_model(changeset, model_cls)
|
|
662
|
+
self.dispatcher.dispatch(model_changeset, f"after_{event_prefix}", bypass_hooks=False)
|
|
637
663
|
|
|
638
664
|
return result
|
|
639
665
|
|
|
@@ -655,11 +681,13 @@ class BulkOperationCoordinator:
|
|
|
655
681
|
for field_name in update_kwargs.keys():
|
|
656
682
|
try:
|
|
657
683
|
field = self.model_cls._meta.get_field(field_name)
|
|
658
|
-
if (
|
|
659
|
-
|
|
660
|
-
not field.
|
|
661
|
-
|
|
662
|
-
field
|
|
684
|
+
if (
|
|
685
|
+
field.is_relation
|
|
686
|
+
and not field.many_to_many
|
|
687
|
+
and not field.one_to_many
|
|
688
|
+
and hasattr(field, "attname")
|
|
689
|
+
and field.attname == field_name
|
|
690
|
+
):
|
|
663
691
|
# This is a FK field being updated by its attname (e.g., business_id)
|
|
664
692
|
# Add the relationship name (e.g., 'business') to skip list
|
|
665
693
|
fk_relationships.add(field.name)
|
|
@@ -668,3 +696,107 @@ class BulkOperationCoordinator:
|
|
|
668
696
|
continue
|
|
669
697
|
|
|
670
698
|
return fk_relationships
|
|
699
|
+
|
|
700
|
+
def _is_upsert_operation(self, result_objects):
|
|
701
|
+
"""
|
|
702
|
+
Check if the operation was an upsert (mixed create/update).
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
result_objects: List of objects returned from the operation
|
|
706
|
+
|
|
707
|
+
Returns:
|
|
708
|
+
True if this was an upsert operation, False otherwise
|
|
709
|
+
"""
|
|
710
|
+
if not result_objects:
|
|
711
|
+
return False
|
|
712
|
+
|
|
713
|
+
# Check if any object has upsert metadata
|
|
714
|
+
return hasattr(result_objects[0], "_bulk_hooks_upsert_metadata")
|
|
715
|
+
|
|
716
|
+
def _dispatch_upsert_after_hooks(self, result_objects, models_in_chain):
|
|
717
|
+
"""
|
|
718
|
+
Dispatch after hooks for upsert operations, splitting by create/update.
|
|
719
|
+
|
|
720
|
+
This matches Salesforce behavior:
|
|
721
|
+
- Records that were created fire after_create hooks
|
|
722
|
+
- Records that were updated fire after_update hooks
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
result_objects: List of objects returned from the operation
|
|
726
|
+
models_in_chain: List of model classes in the MTI inheritance chain
|
|
727
|
+
"""
|
|
728
|
+
# Split objects by operation type
|
|
729
|
+
created_objects = []
|
|
730
|
+
updated_objects = []
|
|
731
|
+
missing_metadata_count = 0
|
|
732
|
+
|
|
733
|
+
for obj in result_objects:
|
|
734
|
+
# Check if metadata was set (it MUST be set for upsert operations)
|
|
735
|
+
if not hasattr(obj, '_bulk_hooks_upsert_metadata'):
|
|
736
|
+
# This should never happen - log and treat as created to maintain backward compat
|
|
737
|
+
missing_metadata_count += 1
|
|
738
|
+
logger.warning(
|
|
739
|
+
f"Object {obj} (id={id(obj)}, pk={getattr(obj, 'pk', None)}) "
|
|
740
|
+
f"missing upsert metadata - defaulting to 'created'. "
|
|
741
|
+
f"This may indicate a bug in the upsert metadata tagging.",
|
|
742
|
+
)
|
|
743
|
+
was_created = True
|
|
744
|
+
else:
|
|
745
|
+
was_created = getattr(obj, "_bulk_hooks_was_created", True)
|
|
746
|
+
|
|
747
|
+
if was_created:
|
|
748
|
+
created_objects.append(obj)
|
|
749
|
+
else:
|
|
750
|
+
updated_objects.append(obj)
|
|
751
|
+
|
|
752
|
+
if missing_metadata_count > 0:
|
|
753
|
+
logger.error(
|
|
754
|
+
f"UPSERT METADATA BUG: {missing_metadata_count}/{len(result_objects)} objects "
|
|
755
|
+
f"missing metadata. This will cause incorrect hook firing!",
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
logger.info(f"Upsert after hooks: {len(created_objects)} created, {len(updated_objects)} updated")
|
|
759
|
+
|
|
760
|
+
# Dispatch after_create hooks for created objects
|
|
761
|
+
if created_objects:
|
|
762
|
+
from django_bulk_hooks.helpers import build_changeset_for_create
|
|
763
|
+
|
|
764
|
+
create_changeset = build_changeset_for_create(self.model_cls, created_objects)
|
|
765
|
+
|
|
766
|
+
for model_cls in models_in_chain:
|
|
767
|
+
model_changeset = self._build_changeset_for_model(create_changeset, model_cls)
|
|
768
|
+
self.dispatcher.dispatch(model_changeset, "after_create", bypass_hooks=False)
|
|
769
|
+
|
|
770
|
+
# Dispatch after_update hooks for updated objects
|
|
771
|
+
if updated_objects:
|
|
772
|
+
# Fetch old records for proper change detection
|
|
773
|
+
old_records_map = self.analyzer.fetch_old_records_map(updated_objects)
|
|
774
|
+
|
|
775
|
+
from django_bulk_hooks.helpers import build_changeset_for_update
|
|
776
|
+
|
|
777
|
+
update_changeset = build_changeset_for_update(
|
|
778
|
+
self.model_cls,
|
|
779
|
+
updated_objects,
|
|
780
|
+
update_kwargs={}, # Empty since we don't know specific fields
|
|
781
|
+
old_records_map=old_records_map,
|
|
782
|
+
)
|
|
783
|
+
|
|
784
|
+
for model_cls in models_in_chain:
|
|
785
|
+
model_changeset = self._build_changeset_for_model(update_changeset, model_cls)
|
|
786
|
+
self.dispatcher.dispatch(model_changeset, "after_update", bypass_hooks=False)
|
|
787
|
+
|
|
788
|
+
# Clean up temporary metadata
|
|
789
|
+
self._cleanup_upsert_metadata(result_objects)
|
|
790
|
+
|
|
791
|
+
def _cleanup_upsert_metadata(self, result_objects):
|
|
792
|
+
"""
|
|
793
|
+
Clean up temporary metadata added during upsert operations.
|
|
794
|
+
|
|
795
|
+
Args:
|
|
796
|
+
result_objects: List of objects to clean up
|
|
797
|
+
"""
|
|
798
|
+
for obj in result_objects:
|
|
799
|
+
if hasattr(obj, "_bulk_hooks_was_created"):
|
|
800
|
+
delattr(obj, "_bulk_hooks_was_created")
|
|
801
|
+
if hasattr(obj, "_bulk_hooks_upsert_metadata"):
|
|
802
|
+
delattr(obj, "_bulk_hooks_upsert_metadata")
|
|
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.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/operations/__init__.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/operations/analyzer.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/operations/mti_handler.py
RENAMED
|
File without changes
|
{django_bulk_hooks-0.2.43 → django_bulk_hooks-0.2.45}/django_bulk_hooks/operations/mti_plans.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|