django-bulk-hooks 0.2.42__py3-none-any.whl → 0.2.50__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/operations/analyzer.py +22 -25
- django_bulk_hooks/operations/bulk_executor.py +180 -124
- django_bulk_hooks/operations/coordinator.py +191 -41
- django_bulk_hooks/operations/mti_handler.py +75 -46
- django_bulk_hooks/operations/mti_plans.py +9 -6
- django_bulk_hooks/operations/record_classifier.py +26 -21
- django_bulk_hooks/registry.py +1 -0
- {django_bulk_hooks-0.2.42.dist-info → django_bulk_hooks-0.2.50.dist-info}/METADATA +1 -1
- {django_bulk_hooks-0.2.42.dist-info → django_bulk_hooks-0.2.50.dist-info}/RECORD +11 -11
- {django_bulk_hooks-0.2.42.dist-info → django_bulk_hooks-0.2.50.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.2.42.dist-info → django_bulk_hooks-0.2.50.dist-info}/WHEEL +0 -0
|
@@ -14,7 +14,7 @@ from typing import Any
|
|
|
14
14
|
class ParentLevel:
|
|
15
15
|
"""
|
|
16
16
|
Represents one level in the parent hierarchy for MTI bulk create.
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
Attributes:
|
|
19
19
|
model_class: The parent model class for this level
|
|
20
20
|
objects: List of parent instances to create
|
|
@@ -23,6 +23,7 @@ class ParentLevel:
|
|
|
23
23
|
unique_fields: Fields for conflict detection (if update_conflicts=True)
|
|
24
24
|
update_fields: Fields to update on conflict (if update_conflicts=True)
|
|
25
25
|
"""
|
|
26
|
+
|
|
26
27
|
model_class: Any
|
|
27
28
|
objects: list[Any]
|
|
28
29
|
original_object_map: dict[int, int] = field(default_factory=dict)
|
|
@@ -35,10 +36,10 @@ class ParentLevel:
|
|
|
35
36
|
class MTICreatePlan:
|
|
36
37
|
"""
|
|
37
38
|
Plan for executing bulk_create on an MTI model.
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
This plan describes WHAT to create, not HOW to create it.
|
|
40
41
|
The executor is responsible for executing this plan.
|
|
41
|
-
|
|
42
|
+
|
|
42
43
|
Attributes:
|
|
43
44
|
inheritance_chain: List of model classes from root to child
|
|
44
45
|
parent_levels: List of ParentLevel objects, one per parent model
|
|
@@ -51,6 +52,7 @@ class MTICreatePlan:
|
|
|
51
52
|
unique_fields: Fields used for conflict detection
|
|
52
53
|
update_fields: Fields to update on conflict
|
|
53
54
|
"""
|
|
55
|
+
|
|
54
56
|
inheritance_chain: list[Any]
|
|
55
57
|
parent_levels: list[ParentLevel]
|
|
56
58
|
child_objects: list[Any]
|
|
@@ -67,12 +69,13 @@ class MTICreatePlan:
|
|
|
67
69
|
class ModelFieldGroup:
|
|
68
70
|
"""
|
|
69
71
|
Represents fields to update for one model in the inheritance chain.
|
|
70
|
-
|
|
72
|
+
|
|
71
73
|
Attributes:
|
|
72
74
|
model_class: The model class
|
|
73
75
|
fields: List of field names to update on this model
|
|
74
76
|
filter_field: Field to use for filtering (e.g., 'pk' or parent link attname)
|
|
75
77
|
"""
|
|
78
|
+
|
|
76
79
|
model_class: Any
|
|
77
80
|
fields: list[str]
|
|
78
81
|
filter_field: str = "pk"
|
|
@@ -82,15 +85,15 @@ class ModelFieldGroup:
|
|
|
82
85
|
class MTIUpdatePlan:
|
|
83
86
|
"""
|
|
84
87
|
Plan for executing bulk_update on an MTI model.
|
|
85
|
-
|
|
88
|
+
|
|
86
89
|
Attributes:
|
|
87
90
|
inheritance_chain: List of model classes from root to child
|
|
88
91
|
field_groups: List of ModelFieldGroup objects
|
|
89
92
|
objects: Objects to update
|
|
90
93
|
batch_size: Batch size for operations
|
|
91
94
|
"""
|
|
95
|
+
|
|
92
96
|
inheritance_chain: list[Any]
|
|
93
97
|
field_groups: list[ModelFieldGroup]
|
|
94
98
|
objects: list[Any]
|
|
95
99
|
batch_size: int = None
|
|
96
|
-
|
|
@@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
|
|
|
17
17
|
class RecordClassifier:
|
|
18
18
|
"""
|
|
19
19
|
Service for classifying and fetching records via database queries.
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
This is the SINGLE point of truth for record classification queries.
|
|
22
22
|
Keeps database access logic separate from business/planning logic.
|
|
23
23
|
"""
|
|
@@ -31,17 +31,18 @@ class RecordClassifier:
|
|
|
31
31
|
"""
|
|
32
32
|
self.model_cls = model_cls
|
|
33
33
|
|
|
34
|
-
def classify_for_upsert(self, objs, unique_fields):
|
|
34
|
+
def classify_for_upsert(self, objs, unique_fields, query_model=None):
|
|
35
35
|
"""
|
|
36
36
|
Classify records as new or existing based on unique_fields.
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
Queries the database to check which records already exist based on the
|
|
39
39
|
unique_fields constraint.
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
Args:
|
|
42
42
|
objs: List of model instances
|
|
43
43
|
unique_fields: List of field names that form the unique constraint
|
|
44
|
-
|
|
44
|
+
query_model: Optional model class to query (for MTI, may be different from self.model_cls)
|
|
45
|
+
|
|
45
46
|
Returns:
|
|
46
47
|
Tuple of (existing_record_ids, existing_pks_map)
|
|
47
48
|
- existing_record_ids: Set of id() for objects that exist in DB
|
|
@@ -50,6 +51,9 @@ class RecordClassifier:
|
|
|
50
51
|
if not unique_fields or not objs:
|
|
51
52
|
return set(), {}
|
|
52
53
|
|
|
54
|
+
# Use query_model if provided (for MTI scenarios), otherwise use self.model_cls
|
|
55
|
+
query_model = query_model or self.model_cls
|
|
56
|
+
|
|
53
57
|
# Build a query to find existing records
|
|
54
58
|
queries = []
|
|
55
59
|
obj_to_unique_values = {}
|
|
@@ -77,9 +81,12 @@ class RecordClassifier:
|
|
|
77
81
|
for q in queries[1:]:
|
|
78
82
|
combined_query |= q
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
)
|
|
84
|
+
logger.info(f"Classifying for upsert: model={query_model.__name__}, query={combined_query}, unique_fields={unique_fields}")
|
|
85
|
+
queryset = query_model.objects.filter(combined_query)
|
|
86
|
+
logger.info(f"Queryset SQL: {queryset.query}")
|
|
87
|
+
logger.info(f"All records in table: {query_model.objects.all().count()}")
|
|
88
|
+
existing_records = list(queryset.values("pk", *unique_fields))
|
|
89
|
+
logger.info(f"Found {len(existing_records)} existing records: {existing_records}")
|
|
83
90
|
|
|
84
91
|
# Map existing records back to original objects
|
|
85
92
|
existing_record_ids = set()
|
|
@@ -94,8 +101,7 @@ class RecordClassifier:
|
|
|
94
101
|
existing_pks_map[obj_id] = record["pk"]
|
|
95
102
|
|
|
96
103
|
logger.info(
|
|
97
|
-
f"Classified {len(existing_record_ids)} existing and "
|
|
98
|
-
f"{len(objs) - len(existing_record_ids)} new records for upsert",
|
|
104
|
+
f"Classified {len(existing_record_ids)} existing and {len(objs) - len(existing_record_ids)} new records for upsert",
|
|
99
105
|
)
|
|
100
106
|
|
|
101
107
|
return existing_record_ids, existing_pks_map
|
|
@@ -103,12 +109,12 @@ class RecordClassifier:
|
|
|
103
109
|
def fetch_by_pks(self, pks, select_related=None, prefetch_related=None):
|
|
104
110
|
"""
|
|
105
111
|
Fetch records by primary keys with optional relationship loading.
|
|
106
|
-
|
|
112
|
+
|
|
107
113
|
Args:
|
|
108
114
|
pks: List of primary key values
|
|
109
115
|
select_related: Optional list of fields to select_related
|
|
110
116
|
prefetch_related: Optional list of fields to prefetch_related
|
|
111
|
-
|
|
117
|
+
|
|
112
118
|
Returns:
|
|
113
119
|
Dict[pk, instance] for O(1) lookups
|
|
114
120
|
"""
|
|
@@ -128,10 +134,10 @@ class RecordClassifier:
|
|
|
128
134
|
def fetch_by_unique_constraint(self, field_values_map):
|
|
129
135
|
"""
|
|
130
136
|
Fetch records matching a unique constraint.
|
|
131
|
-
|
|
137
|
+
|
|
132
138
|
Args:
|
|
133
139
|
field_values_map: Dict of {field_name: value} for unique constraint
|
|
134
|
-
|
|
140
|
+
|
|
135
141
|
Returns:
|
|
136
142
|
Model instance if found, None otherwise
|
|
137
143
|
"""
|
|
@@ -141,18 +147,17 @@ class RecordClassifier:
|
|
|
141
147
|
return None
|
|
142
148
|
except self.model_cls.MultipleObjectsReturned:
|
|
143
149
|
logger.warning(
|
|
144
|
-
f"Multiple {self.model_cls.__name__} records found for "
|
|
145
|
-
f"unique constraint {field_values_map}",
|
|
150
|
+
f"Multiple {self.model_cls.__name__} records found for unique constraint {field_values_map}",
|
|
146
151
|
)
|
|
147
152
|
return self.model_cls.objects.filter(**field_values_map).first()
|
|
148
153
|
|
|
149
154
|
def exists_by_pks(self, pks):
|
|
150
155
|
"""
|
|
151
156
|
Check if records exist by primary keys without fetching them.
|
|
152
|
-
|
|
157
|
+
|
|
153
158
|
Args:
|
|
154
159
|
pks: List of primary key values
|
|
155
|
-
|
|
160
|
+
|
|
156
161
|
Returns:
|
|
157
162
|
Set of PKs that exist in the database
|
|
158
163
|
"""
|
|
@@ -168,13 +173,13 @@ class RecordClassifier:
|
|
|
168
173
|
def count_by_unique_fields(self, objs, unique_fields):
|
|
169
174
|
"""
|
|
170
175
|
Count how many objects already exist based on unique fields.
|
|
171
|
-
|
|
176
|
+
|
|
172
177
|
Useful for validation or reporting before upsert operations.
|
|
173
|
-
|
|
178
|
+
|
|
174
179
|
Args:
|
|
175
180
|
objs: List of model instances
|
|
176
181
|
unique_fields: List of field names that form the unique constraint
|
|
177
|
-
|
|
182
|
+
|
|
178
183
|
Returns:
|
|
179
184
|
Tuple of (existing_count, new_count)
|
|
180
185
|
"""
|
django_bulk_hooks/registry.py
CHANGED
|
@@ -12,15 +12,15 @@ django_bulk_hooks/helpers.py,sha256=Nw8eXryLUUquW7AgiuKp0PQT3Pq6HAHsdP-xAtqhmjA,
|
|
|
12
12
|
django_bulk_hooks/manager.py,sha256=3mFzB0ZzHHeXWdKGObZD_H0NlskHJc8uYBF69KKdAXU,4068
|
|
13
13
|
django_bulk_hooks/models.py,sha256=4Vvi2LiGP0g4j08a5liqBROfsO8Wd_ermBoyjKwfrPU,2512
|
|
14
14
|
django_bulk_hooks/operations/__init__.py,sha256=BtJYjmRhe_sScivLsniDaZmBkm0ZLvcmzXFKL7QY2Xg,550
|
|
15
|
-
django_bulk_hooks/operations/analyzer.py,sha256=
|
|
16
|
-
django_bulk_hooks/operations/bulk_executor.py,sha256=
|
|
17
|
-
django_bulk_hooks/operations/coordinator.py,sha256=
|
|
18
|
-
django_bulk_hooks/operations/mti_handler.py,sha256=
|
|
19
|
-
django_bulk_hooks/operations/mti_plans.py,sha256=
|
|
20
|
-
django_bulk_hooks/operations/record_classifier.py,sha256=
|
|
15
|
+
django_bulk_hooks/operations/analyzer.py,sha256=wAG8sAG9NwfwNqG9z81VfGR7AANDzRmMGE_o82MWji4,10689
|
|
16
|
+
django_bulk_hooks/operations/bulk_executor.py,sha256=kqRGSdqKHc1WoG8yEhn82gKZ9KelV3GGTU7YccUdFG0,24737
|
|
17
|
+
django_bulk_hooks/operations/coordinator.py,sha256=iGavJLqe3eYRqFay8cMn6muwyRYzQo-HFGphsS5hL6g,30799
|
|
18
|
+
django_bulk_hooks/operations/mti_handler.py,sha256=uDrfE29e80v4YjPF4fPw2x0bntRnFGyfEh-grMQUGu0,20907
|
|
19
|
+
django_bulk_hooks/operations/mti_plans.py,sha256=7STQ2oA2ZT8cEG3-t-6xciRAdf7OeSf0gRLXR_BRG-Q,3363
|
|
20
|
+
django_bulk_hooks/operations/record_classifier.py,sha256=vNi0WSNiPAVb8pTZZJ26b81oX59snk7LIxWMzyenDCk,6694
|
|
21
21
|
django_bulk_hooks/queryset.py,sha256=aQitlbexcVnmeAdc0jtO3hci39p4QEu4srQPEzozy5s,5546
|
|
22
|
-
django_bulk_hooks/registry.py,sha256=
|
|
23
|
-
django_bulk_hooks-0.2.
|
|
24
|
-
django_bulk_hooks-0.2.
|
|
25
|
-
django_bulk_hooks-0.2.
|
|
26
|
-
django_bulk_hooks-0.2.
|
|
22
|
+
django_bulk_hooks/registry.py,sha256=uum5jhGI3TPaoiXuA1MdBdu4gbE3rQGGwQ5YDjiMcjk,7949
|
|
23
|
+
django_bulk_hooks-0.2.50.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
24
|
+
django_bulk_hooks-0.2.50.dist-info/METADATA,sha256=oVFxbFoCH691HY57pcgVa9em9_M0IAg1L0VECri_l_Q,9265
|
|
25
|
+
django_bulk_hooks-0.2.50.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
26
|
+
django_bulk_hooks-0.2.50.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|