django-bulk-hooks 0.1.205__py3-none-any.whl → 0.1.207__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 +70 -32
- {django_bulk_hooks-0.1.205.dist-info → django_bulk_hooks-0.1.207.dist-info}/METADATA +43 -3
- {django_bulk_hooks-0.1.205.dist-info → django_bulk_hooks-0.1.207.dist-info}/RECORD +5 -5
- {django_bulk_hooks-0.1.205.dist-info → django_bulk_hooks-0.1.207.dist-info}/WHEEL +1 -1
- {django_bulk_hooks-0.1.205.dist-info → django_bulk_hooks-0.1.207.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,26 @@ 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
|
+
# Single bulk query to get all updated values
|
|
86
|
+
# Use values() to get only the fields we need for maximum performance
|
|
87
|
+
field_names = [f.name for f in model_cls._meta.fields if f.name != "id"]
|
|
88
|
+
refreshed_data = {
|
|
89
|
+
obj["pk"]: obj
|
|
90
|
+
for obj in model_cls._base_manager.filter(pk__in=pks).values(
|
|
91
|
+
"pk", *field_names
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Bulk update all instances in memory
|
|
96
|
+
for instance in instances:
|
|
97
|
+
if instance.pk in refreshed_data:
|
|
98
|
+
data = refreshed_data[instance.pk]
|
|
99
|
+
# Bulk copy all field values except primary key
|
|
100
|
+
for field_name in field_names:
|
|
101
|
+
setattr(instance, field_name, data[field_name])
|
|
102
|
+
|
|
78
103
|
# Run AFTER_UPDATE hooks
|
|
79
104
|
engine.run(model_cls, AFTER_UPDATE, instances, originals, ctx=ctx)
|
|
80
105
|
|
|
@@ -303,14 +328,14 @@ class HookQuerySetMixin:
|
|
|
303
328
|
while current_model:
|
|
304
329
|
if not current_model._meta.proxy:
|
|
305
330
|
chain.append(current_model)
|
|
306
|
-
|
|
331
|
+
|
|
307
332
|
parents = [
|
|
308
333
|
parent
|
|
309
334
|
for parent in current_model._meta.parents.keys()
|
|
310
335
|
if not parent._meta.proxy
|
|
311
336
|
]
|
|
312
337
|
current_model = parents[0] if parents else None
|
|
313
|
-
|
|
338
|
+
|
|
314
339
|
chain.reverse()
|
|
315
340
|
return chain
|
|
316
341
|
|
|
@@ -584,10 +609,10 @@ class HookQuerySetMixin:
|
|
|
584
609
|
for field in model._meta.local_fields:
|
|
585
610
|
if hasattr(field, "auto_now") and field.auto_now:
|
|
586
611
|
auto_now_fields.add(field.name)
|
|
587
|
-
|
|
612
|
+
|
|
588
613
|
# Combine original fields with auto_now fields
|
|
589
614
|
all_fields = list(fields) + list(auto_now_fields)
|
|
590
|
-
|
|
615
|
+
|
|
591
616
|
# Group fields by model in the inheritance chain
|
|
592
617
|
field_groups = {}
|
|
593
618
|
for field_name in all_fields:
|
|
@@ -603,7 +628,7 @@ class HookQuerySetMixin:
|
|
|
603
628
|
# Process in batches
|
|
604
629
|
batch_size = django_kwargs.get("batch_size") or len(objs)
|
|
605
630
|
total_updated = 0
|
|
606
|
-
|
|
631
|
+
|
|
607
632
|
with transaction.atomic(using=self.db, savepoint=False):
|
|
608
633
|
for i in range(0, len(objs), batch_size):
|
|
609
634
|
batch = objs[i : i + batch_size]
|
|
@@ -614,35 +639,37 @@ class HookQuerySetMixin:
|
|
|
614
639
|
|
|
615
640
|
return total_updated
|
|
616
641
|
|
|
617
|
-
def _process_mti_bulk_update_batch(
|
|
642
|
+
def _process_mti_bulk_update_batch(
|
|
643
|
+
self, batch, field_groups, inheritance_chain, **kwargs
|
|
644
|
+
):
|
|
618
645
|
"""
|
|
619
646
|
Process a single batch of objects for MTI bulk update.
|
|
620
647
|
Updates each table in the inheritance chain for the batch.
|
|
621
648
|
"""
|
|
622
649
|
total_updated = 0
|
|
623
|
-
|
|
650
|
+
|
|
624
651
|
# For MTI, we need to handle parent links correctly
|
|
625
652
|
# The root model (first in chain) has its own PK
|
|
626
653
|
# Child models use the parent link to reference the root PK
|
|
627
654
|
root_model = inheritance_chain[0]
|
|
628
|
-
|
|
655
|
+
|
|
629
656
|
# Get the primary keys from the objects
|
|
630
657
|
# If objects have pk set but are not loaded from DB, use those PKs
|
|
631
658
|
root_pks = []
|
|
632
659
|
for obj in batch:
|
|
633
660
|
# Check both pk and id attributes
|
|
634
|
-
pk_value = getattr(obj,
|
|
661
|
+
pk_value = getattr(obj, "pk", None)
|
|
635
662
|
if pk_value is None:
|
|
636
|
-
pk_value = getattr(obj,
|
|
637
|
-
|
|
663
|
+
pk_value = getattr(obj, "id", None)
|
|
664
|
+
|
|
638
665
|
if pk_value is not None:
|
|
639
666
|
root_pks.append(pk_value)
|
|
640
667
|
else:
|
|
641
668
|
continue
|
|
642
|
-
|
|
669
|
+
|
|
643
670
|
if not root_pks:
|
|
644
671
|
return 0
|
|
645
|
-
|
|
672
|
+
|
|
646
673
|
# Update each table in the inheritance chain
|
|
647
674
|
for model, model_fields in field_groups.items():
|
|
648
675
|
if not model_fields:
|
|
@@ -651,7 +678,7 @@ class HookQuerySetMixin:
|
|
|
651
678
|
if model == inheritance_chain[0]:
|
|
652
679
|
# Root model - use primary keys directly
|
|
653
680
|
pks = root_pks
|
|
654
|
-
filter_field =
|
|
681
|
+
filter_field = "pk"
|
|
655
682
|
else:
|
|
656
683
|
# Child model - use parent link field
|
|
657
684
|
parent_link = None
|
|
@@ -659,48 +686,58 @@ class HookQuerySetMixin:
|
|
|
659
686
|
if parent_model in model._meta.parents:
|
|
660
687
|
parent_link = model._meta.parents[parent_model]
|
|
661
688
|
break
|
|
662
|
-
|
|
689
|
+
|
|
663
690
|
if parent_link is None:
|
|
664
691
|
continue
|
|
665
|
-
|
|
692
|
+
|
|
666
693
|
# For child models, the parent link values should be the same as root PKs
|
|
667
694
|
pks = root_pks
|
|
668
695
|
filter_field = parent_link.attname
|
|
669
|
-
|
|
696
|
+
|
|
670
697
|
if pks:
|
|
671
698
|
base_qs = model._base_manager.using(self.db)
|
|
672
|
-
|
|
699
|
+
|
|
673
700
|
# Check if records exist
|
|
674
701
|
existing_count = base_qs.filter(**{f"{filter_field}__in": pks}).count()
|
|
675
|
-
|
|
702
|
+
|
|
676
703
|
if existing_count == 0:
|
|
677
704
|
continue
|
|
678
|
-
|
|
705
|
+
|
|
679
706
|
# Build CASE statements for each field to perform a single bulk update
|
|
680
707
|
case_statements = {}
|
|
681
708
|
for field_name in model_fields:
|
|
682
709
|
field = model._meta.get_field(field_name)
|
|
683
710
|
when_statements = []
|
|
684
|
-
|
|
711
|
+
|
|
685
712
|
for pk, obj in zip(pks, batch):
|
|
686
713
|
# Check both pk and id attributes for the object
|
|
687
|
-
obj_pk = getattr(obj,
|
|
714
|
+
obj_pk = getattr(obj, "pk", None)
|
|
688
715
|
if obj_pk is None:
|
|
689
|
-
obj_pk = getattr(obj,
|
|
690
|
-
|
|
716
|
+
obj_pk = getattr(obj, "id", None)
|
|
717
|
+
|
|
691
718
|
if obj_pk is None:
|
|
692
719
|
continue
|
|
693
720
|
value = getattr(obj, field_name)
|
|
694
|
-
when_statements.append(
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
721
|
+
when_statements.append(
|
|
722
|
+
When(
|
|
723
|
+
**{filter_field: pk},
|
|
724
|
+
then=Value(value, output_field=field),
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
case_statements[field_name] = Case(
|
|
729
|
+
*when_statements, output_field=field
|
|
730
|
+
)
|
|
731
|
+
|
|
698
732
|
# Execute a single bulk update for all objects in this model
|
|
699
733
|
try:
|
|
700
|
-
updated_count = base_qs.filter(
|
|
734
|
+
updated_count = base_qs.filter(
|
|
735
|
+
**{f"{filter_field}__in": pks}
|
|
736
|
+
).update(**case_statements)
|
|
701
737
|
total_updated += updated_count
|
|
702
738
|
except Exception as e:
|
|
703
739
|
import traceback
|
|
740
|
+
|
|
704
741
|
traceback.print_exc()
|
|
705
742
|
|
|
706
743
|
return total_updated
|
|
@@ -711,4 +748,5 @@ class HookQuerySet(HookQuerySetMixin, models.QuerySet):
|
|
|
711
748
|
A QuerySet that provides bulk hook functionality.
|
|
712
749
|
This is the traditional approach for backward compatibility.
|
|
713
750
|
"""
|
|
751
|
+
|
|
714
752
|
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.207
|
|
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=YMw8Za_1aqLkCmTqhYKnznq95IVlie3Wx9NBDND7KcM,31387
|
|
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.207.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
15
|
+
django_bulk_hooks-0.1.207.dist-info/METADATA,sha256=6XzEu4cdHtO1kVPzw-vuUmoFjbEUaVrTdEEKAFqEP60,9049
|
|
16
|
+
django_bulk_hooks-0.1.207.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
17
|
+
django_bulk_hooks-0.1.207.dist-info/RECORD,,
|
|
File without changes
|