django-bulk-hooks 0.1.206__py3-none-any.whl → 0.1.208__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/queryset.py +71 -32
- {django_bulk_hooks-0.1.206.dist-info → django_bulk_hooks-0.1.208.dist-info}/METADATA +43 -3
- {django_bulk_hooks-0.1.206.dist-info → django_bulk_hooks-0.1.208.dist-info}/RECORD +5 -5
- {django_bulk_hooks-0.1.206.dist-info → django_bulk_hooks-0.1.208.dist-info}/WHEEL +1 -1
- {django_bulk_hooks-0.1.206.dist-info → django_bulk_hooks-0.1.208.dist-info}/LICENSE +0 -0
django_bulk_hooks/queryset.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from django.db import models, transaction
|
|
2
|
-
from django.db.models import AutoField
|
|
2
|
+
from django.db.models import AutoField, Case, Field, Value, When
|
|
3
3
|
|
|
4
4
|
from django_bulk_hooks import engine
|
|
5
5
|
from django_bulk_hooks.constants import (
|
|
@@ -14,7 +14,6 @@ from django_bulk_hooks.constants import (
|
|
|
14
14
|
VALIDATE_UPDATE,
|
|
15
15
|
)
|
|
16
16
|
from django_bulk_hooks.context import HookContext
|
|
17
|
-
from django.db.models import When, Value, Case, Field
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
class HookQuerySetMixin:
|
|
@@ -22,7 +21,7 @@ class HookQuerySetMixin:
|
|
|
22
21
|
A mixin that provides bulk hook functionality to any QuerySet.
|
|
23
22
|
This can be dynamically injected into querysets from other managers.
|
|
24
23
|
"""
|
|
25
|
-
|
|
24
|
+
|
|
26
25
|
@transaction.atomic
|
|
27
26
|
def delete(self):
|
|
28
27
|
objs = list(self)
|
|
@@ -62,6 +61,12 @@ class HookQuerySetMixin:
|
|
|
62
61
|
}
|
|
63
62
|
originals = [original_map.get(obj.pk) for obj in instances]
|
|
64
63
|
|
|
64
|
+
# Check if any of the update values are Subquery objects
|
|
65
|
+
has_subquery = any(
|
|
66
|
+
hasattr(value, "query") and hasattr(value, "resolve_expression")
|
|
67
|
+
for value in kwargs.values()
|
|
68
|
+
)
|
|
69
|
+
|
|
65
70
|
# Apply field updates to instances
|
|
66
71
|
for obj in instances:
|
|
67
72
|
for field, value in kwargs.items():
|
|
@@ -75,6 +80,27 @@ class HookQuerySetMixin:
|
|
|
75
80
|
# Call the base QuerySet implementation to avoid recursion
|
|
76
81
|
update_count = super().update(**kwargs)
|
|
77
82
|
|
|
83
|
+
# If we used Subquery objects, refresh the instances to get computed values
|
|
84
|
+
if has_subquery and instances:
|
|
85
|
+
# Simple refresh of model fields without fetching related objects
|
|
86
|
+
# Subquery updates only affect the model's own fields, not relationships
|
|
87
|
+
refreshed_instances = {
|
|
88
|
+
obj.pk: obj for obj in model_cls._base_manager.filter(pk__in=pks)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Bulk update all instances in memory
|
|
92
|
+
for instance in instances:
|
|
93
|
+
if instance.pk in refreshed_instances:
|
|
94
|
+
refreshed_instance = refreshed_instances[instance.pk]
|
|
95
|
+
# Update all fields except primary key
|
|
96
|
+
for field in model_cls._meta.fields:
|
|
97
|
+
if field.name != "id":
|
|
98
|
+
setattr(
|
|
99
|
+
instance,
|
|
100
|
+
field.name,
|
|
101
|
+
getattr(refreshed_instance, field.name),
|
|
102
|
+
)
|
|
103
|
+
|
|
78
104
|
# Run AFTER_UPDATE hooks
|
|
79
105
|
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
80
106
|
|
|
@@ -303,14 +329,14 @@ class HookQuerySetMixin:
|
|
|
303
329
|
while current_model:
|
|
304
330
|
if not current_model._meta.proxy:
|
|
305
331
|
chain.append(current_model)
|
|
306
|
-
|
|
332
|
+
|
|
307
333
|
parents = [
|
|
308
334
|
parent
|
|
309
335
|
for parent in current_model._meta.parents.keys()
|
|
310
336
|
if not parent._meta.proxy
|
|
311
337
|
]
|
|
312
338
|
current_model = parents[0] if parents else None
|
|
313
|
-
|
|
339
|
+
|
|
314
340
|
chain.reverse()
|
|
315
341
|
return chain
|
|
316
342
|
|
|
@@ -584,10 +610,10 @@ class HookQuerySetMixin:
|
|
|
584
610
|
for field in model._meta.local_fields:
|
|
585
611
|
if hasattr(field, "auto_now") and field.auto_now:
|
|
586
612
|
auto_now_fields.add(field.name)
|
|
587
|
-
|
|
613
|
+
|
|
588
614
|
# Combine original fields with auto_now fields
|
|
589
615
|
all_fields = list(fields) + list(auto_now_fields)
|
|
590
|
-
|
|
616
|
+
|
|
591
617
|
# Group fields by model in the inheritance chain
|
|
592
618
|
field_groups = {}
|
|
593
619
|
for field_name in all_fields:
|
|
@@ -603,7 +629,7 @@ class HookQuerySetMixin:
|
|
|
603
629
|
# Process in batches
|
|
604
630
|
batch_size = django_kwargs.get("batch_size") or len(objs)
|
|
605
631
|
total_updated = 0
|
|
606
|
-
|
|
632
|
+
|
|
607
633
|
with transaction.atomic(using=self.db, savepoint=False):
|
|
608
634
|
for i in range(0, len(objs), batch_size):
|
|
609
635
|
batch = objs[i : i + batch_size]
|
|
@@ -614,35 +640,37 @@ class HookQuerySetMixin:
|
|
|
614
640
|
|
|
615
641
|
return total_updated
|
|
616
642
|
|
|
617
|
-
def _process_mti_bulk_update_batch(
|
|
643
|
+
def _process_mti_bulk_update_batch(
|
|
644
|
+
self, batch, field_groups, inheritance_chain, **kwargs
|
|
645
|
+
):
|
|
618
646
|
"""
|
|
619
647
|
Process a single batch of objects for MTI bulk update.
|
|
620
648
|
Updates each table in the inheritance chain for the batch.
|
|
621
649
|
"""
|
|
622
650
|
total_updated = 0
|
|
623
|
-
|
|
651
|
+
|
|
624
652
|
# For MTI, we need to handle parent links correctly
|
|
625
653
|
# The root model (first in chain) has its own PK
|
|
626
654
|
# Child models use the parent link to reference the root PK
|
|
627
655
|
root_model = inheritance_chain[0]
|
|
628
|
-
|
|
656
|
+
|
|
629
657
|
# Get the primary keys from the objects
|
|
630
658
|
# If objects have pk set but are not loaded from DB, use those PKs
|
|
631
659
|
root_pks = []
|
|
632
660
|
for obj in batch:
|
|
633
661
|
# Check both pk and id attributes
|
|
634
|
-
pk_value = getattr(obj,
|
|
662
|
+
pk_value = getattr(obj, "pk", None)
|
|
635
663
|
if pk_value is None:
|
|
636
|
-
pk_value = getattr(obj,
|
|
637
|
-
|
|
664
|
+
pk_value = getattr(obj, "id", None)
|
|
665
|
+
|
|
638
666
|
if pk_value is not None:
|
|
639
667
|
root_pks.append(pk_value)
|
|
640
668
|
else:
|
|
641
669
|
continue
|
|
642
|
-
|
|
670
|
+
|
|
643
671
|
if not root_pks:
|
|
644
672
|
return 0
|
|
645
|
-
|
|
673
|
+
|
|
646
674
|
# Update each table in the inheritance chain
|
|
647
675
|
for model, model_fields in field_groups.items():
|
|
648
676
|
if not model_fields:
|
|
@@ -651,7 +679,7 @@ class HookQuerySetMixin:
|
|
|
651
679
|
if model == inheritance_chain[0]:
|
|
652
680
|
# Root model - use primary keys directly
|
|
653
681
|
pks = root_pks
|
|
654
|
-
filter_field =
|
|
682
|
+
filter_field = "pk"
|
|
655
683
|
else:
|
|
656
684
|
# Child model - use parent link field
|
|
657
685
|
parent_link = None
|
|
@@ -659,48 +687,58 @@ class HookQuerySetMixin:
|
|
|
659
687
|
if parent_model in model._meta.parents:
|
|
660
688
|
parent_link = model._meta.parents[parent_model]
|
|
661
689
|
break
|
|
662
|
-
|
|
690
|
+
|
|
663
691
|
if parent_link is None:
|
|
664
692
|
continue
|
|
665
|
-
|
|
693
|
+
|
|
666
694
|
# For child models, the parent link values should be the same as root PKs
|
|
667
695
|
pks = root_pks
|
|
668
696
|
filter_field = parent_link.attname
|
|
669
|
-
|
|
697
|
+
|
|
670
698
|
if pks:
|
|
671
699
|
base_qs = model._base_manager.using(self.db)
|
|
672
|
-
|
|
700
|
+
|
|
673
701
|
# Check if records exist
|
|
674
702
|
existing_count = base_qs.filter(**{f"{filter_field}__in": pks}).count()
|
|
675
|
-
|
|
703
|
+
|
|
676
704
|
if existing_count == 0:
|
|
677
705
|
continue
|
|
678
|
-
|
|
706
|
+
|
|
679
707
|
# Build CASE statements for each field to perform a single bulk update
|
|
680
708
|
case_statements = {}
|
|
681
709
|
for field_name in model_fields:
|
|
682
710
|
field = model._meta.get_field(field_name)
|
|
683
711
|
when_statements = []
|
|
684
|
-
|
|
712
|
+
|
|
685
713
|
for pk, obj in zip(pks, batch):
|
|
686
714
|
# Check both pk and id attributes for the object
|
|
687
|
-
obj_pk = getattr(obj,
|
|
715
|
+
obj_pk = getattr(obj, "pk", None)
|
|
688
716
|
if obj_pk is None:
|
|
689
|
-
obj_pk = getattr(obj,
|
|
690
|
-
|
|
717
|
+
obj_pk = getattr(obj, "id", None)
|
|
718
|
+
|
|
691
719
|
if obj_pk is None:
|
|
692
720
|
continue
|
|
693
721
|
value = getattr(obj, field_name)
|
|
694
|
-
when_statements.append(
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
722
|
+
when_statements.append(
|
|
723
|
+
When(
|
|
724
|
+
**{filter_field: pk},
|
|
725
|
+
then=Value(value, output_field=field),
|
|
726
|
+
)
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
case_statements[field_name] = Case(
|
|
730
|
+
*when_statements, output_field=field
|
|
731
|
+
)
|
|
732
|
+
|
|
698
733
|
# Execute a single bulk update for all objects in this model
|
|
699
734
|
try:
|
|
700
|
-
updated_count = base_qs.filter(
|
|
735
|
+
updated_count = base_qs.filter(
|
|
736
|
+
**{f"{filter_field}__in": pks}
|
|
737
|
+
).update(**case_statements)
|
|
701
738
|
total_updated += updated_count
|
|
702
739
|
except Exception as e:
|
|
703
740
|
import traceback
|
|
741
|
+
|
|
704
742
|
traceback.print_exc()
|
|
705
743
|
|
|
706
744
|
return total_updated
|
|
@@ -711,4 +749,5 @@ class HookQuerySet(HookQuerySetMixin, models.QuerySet):
|
|
|
711
749
|
A QuerySet that provides bulk hook functionality.
|
|
712
750
|
This is the traditional approach for backward compatibility.
|
|
713
751
|
"""
|
|
752
|
+
|
|
714
753
|
pass
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: django-bulk-hooks
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.208
|
|
4
4
|
Summary: Hook-style hooks for Django bulk operations like bulk_create and bulk_update.
|
|
5
|
+
Home-page: https://github.com/AugendLimited/django-bulk-hooks
|
|
5
6
|
License: MIT
|
|
6
7
|
Keywords: django,bulk,hooks
|
|
7
8
|
Author: Konrad Beck
|
|
@@ -13,7 +14,6 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
13
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
16
|
Requires-Dist: Django (>=4.0)
|
|
16
|
-
Project-URL: Homepage, https://github.com/AugendLimited/django-bulk-hooks
|
|
17
17
|
Project-URL: Repository, https://github.com/AugendLimited/django-bulk-hooks
|
|
18
18
|
Description-Content-Type: text/markdown
|
|
19
19
|
|
|
@@ -132,6 +132,46 @@ Account.objects.update(balance=0.00)
|
|
|
132
132
|
Account.objects.delete()
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
+
### Subquery Support in Updates
|
|
136
|
+
|
|
137
|
+
When using `Subquery` objects in update operations, the computed values are automatically available in hooks. The system efficiently refreshes all instances in bulk for optimal performance:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from django.db.models import Subquery, OuterRef, Sum
|
|
141
|
+
|
|
142
|
+
def aggregate_revenue_by_ids(self, ids: Iterable[int]) -> int:
|
|
143
|
+
return self.find_by_ids(ids).update(
|
|
144
|
+
revenue=Subquery(
|
|
145
|
+
FinancialTransaction.objects.filter(daily_financial_aggregate_id=OuterRef("pk"))
|
|
146
|
+
.filter(is_revenue=True)
|
|
147
|
+
.values("daily_financial_aggregate_id")
|
|
148
|
+
.annotate(revenue_sum=Sum("amount"))
|
|
149
|
+
.values("revenue_sum")[:1],
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# In your hooks, you can now access the computed revenue value:
|
|
154
|
+
class FinancialAggregateHooks(Hook):
|
|
155
|
+
@hook(AFTER_UPDATE, model=DailyFinancialAggregate)
|
|
156
|
+
def log_revenue_update(self, new_records, old_records):
|
|
157
|
+
for new_record in new_records:
|
|
158
|
+
# This will now contain the computed value, not the Subquery object
|
|
159
|
+
print(f"Updated revenue: {new_record.revenue}")
|
|
160
|
+
|
|
161
|
+
# Bulk operations are optimized for performance:
|
|
162
|
+
def bulk_aggregate_revenue(self, ids: Iterable[int]) -> int:
|
|
163
|
+
# This will efficiently refresh all instances in a single query
|
|
164
|
+
return self.filter(id__in=ids).update(
|
|
165
|
+
revenue=Subquery(
|
|
166
|
+
FinancialTransaction.objects.filter(daily_financial_aggregate_id=OuterRef("pk"))
|
|
167
|
+
.filter(is_revenue=True)
|
|
168
|
+
.values("daily_financial_aggregate_id")
|
|
169
|
+
.annotate(revenue_sum=Sum("amount"))
|
|
170
|
+
.values("revenue_sum")[:1],
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
135
175
|
## 🧠 Why?
|
|
136
176
|
|
|
137
177
|
Django's `bulk_` methods bypass signals and `save()`. This package fills that gap with:
|
|
@@ -9,9 +9,9 @@ django_bulk_hooks/handler.py,sha256=xZt8iNdYF-ACz-MnKMY0co6scWINU5V5wC1lyDn844k,
|
|
|
9
9
|
django_bulk_hooks/manager.py,sha256=nfWiwU5-yAoxdnQsUMohxtyCpkV0MBv6X3wmipr9eQY,3697
|
|
10
10
|
django_bulk_hooks/models.py,sha256=7fnx5xd4HWXfLVlFhhiRzR92JRWFEuxgk6aSWLEsyJg,3996
|
|
11
11
|
django_bulk_hooks/priority.py,sha256=HG_2D35nga68lBCZmSXTcplXrjFoRgZFRDOy4ROKonY,376
|
|
12
|
-
django_bulk_hooks/queryset.py,sha256=
|
|
12
|
+
django_bulk_hooks/queryset.py,sha256=G55Bd0f4Rb_S1Z06lyXvZ4zEYc18R0uor17kfHjdrog,31469
|
|
13
13
|
django_bulk_hooks/registry.py,sha256=-mQBizJ06nz_tajZBinViKx_uP2Tbc1tIpTEMv7lwKA,705
|
|
14
|
-
django_bulk_hooks-0.1.
|
|
15
|
-
django_bulk_hooks-0.1.
|
|
16
|
-
django_bulk_hooks-0.1.
|
|
17
|
-
django_bulk_hooks-0.1.
|
|
14
|
+
django_bulk_hooks-0.1.208.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.208.dist-info/METADATA,sha256=NnIm2Ew0GRwBt3iecx71-RxSZ_x9KJCgFnZklSwPTsU,9049
|
|
16
|
+
django_bulk_hooks-0.1.208.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
+
django_bulk_hooks-0.1.208.dist-info/RECORD,,
|
|
File without changes
|