django-bulk-hooks 0.2.49__py3-none-any.whl → 0.2.51__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/bulk_executor.py +38 -12
- django_bulk_hooks/operations/field_utils.py +94 -0
- django_bulk_hooks/operations/mti_handler.py +9 -18
- django_bulk_hooks/operations/record_classifier.py +9 -2
- {django_bulk_hooks-0.2.49.dist-info → django_bulk_hooks-0.2.51.dist-info}/METADATA +1 -1
- {django_bulk_hooks-0.2.49.dist-info → django_bulk_hooks-0.2.51.dist-info}/RECORD +8 -7
- {django_bulk_hooks-0.2.49.dist-info → django_bulk_hooks-0.2.51.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.2.49.dist-info → django_bulk_hooks-0.2.51.dist-info}/WHEEL +0 -0
|
@@ -10,6 +10,8 @@ from django.db import transaction
|
|
|
10
10
|
from django.db.models import AutoField, ForeignKey, Case, When, Value
|
|
11
11
|
from django.db.models.functions import Cast
|
|
12
12
|
|
|
13
|
+
from django_bulk_hooks.operations.field_utils import get_field_value_for_db
|
|
14
|
+
|
|
13
15
|
logger = logging.getLogger(__name__)
|
|
14
16
|
|
|
15
17
|
|
|
@@ -243,9 +245,11 @@ class BulkExecutor:
|
|
|
243
245
|
# Copy generated fields back to parent objects
|
|
244
246
|
for upserted_parent, parent_obj in zip(upserted_parents, parent_level.objects):
|
|
245
247
|
for field in parent_level.model_class._meta.local_fields:
|
|
246
|
-
|
|
248
|
+
# Use attname for ForeignKey fields to avoid triggering database queries
|
|
249
|
+
field_attr = field.attname if isinstance(field, ForeignKey) else field.name
|
|
250
|
+
upserted_value = getattr(upserted_parent, field_attr, None)
|
|
247
251
|
if upserted_value is not None:
|
|
248
|
-
setattr(parent_obj,
|
|
252
|
+
setattr(parent_obj, field_attr, upserted_value)
|
|
249
253
|
|
|
250
254
|
parent_obj._state.adding = False
|
|
251
255
|
parent_obj._state.db = self.queryset.db
|
|
@@ -291,14 +295,36 @@ class BulkExecutor:
|
|
|
291
295
|
# In MTI, child objects get PKs from parent links, but we need to distinguish
|
|
292
296
|
# between truly new records and existing records for upsert operations
|
|
293
297
|
objs_without_pk, objs_with_pk = [], []
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
298
|
+
|
|
299
|
+
# Check which CHILD records actually exist in the child table
|
|
300
|
+
# This is separate from checking parent existence
|
|
301
|
+
if plan.update_conflicts:
|
|
302
|
+
# Query the CHILD table to see which child records exist
|
|
303
|
+
parent_pks = []
|
|
304
|
+
for child_obj in plan.child_objects:
|
|
305
|
+
child_pk = getattr(child_obj, plan.child_model._meta.pk.attname, None)
|
|
306
|
+
if child_pk:
|
|
307
|
+
parent_pks.append(child_pk)
|
|
308
|
+
|
|
309
|
+
existing_child_pks = set()
|
|
310
|
+
if parent_pks:
|
|
311
|
+
existing_child_pks = set(
|
|
312
|
+
base_qs.filter(pk__in=parent_pks).values_list('pk', flat=True)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Split based on whether child record exists
|
|
316
|
+
for child_obj in plan.child_objects:
|
|
317
|
+
child_pk = getattr(child_obj, plan.child_model._meta.pk.attname, None)
|
|
318
|
+
if child_pk and child_pk in existing_child_pks:
|
|
319
|
+
# Child record exists - update it
|
|
320
|
+
objs_with_pk.append(child_obj)
|
|
321
|
+
else:
|
|
322
|
+
# Child record doesn't exist - insert it
|
|
323
|
+
objs_without_pk.append(child_obj)
|
|
324
|
+
else:
|
|
325
|
+
# Not an upsert - all are new records
|
|
326
|
+
objs_without_pk = plan.child_objects
|
|
327
|
+
objs_with_pk = []
|
|
302
328
|
|
|
303
329
|
# For objects with PK (existing records in upsert), use bulk_update
|
|
304
330
|
if objs_with_pk and plan.update_fields:
|
|
@@ -451,8 +477,8 @@ class BulkExecutor:
|
|
|
451
477
|
if obj_pk is None:
|
|
452
478
|
continue
|
|
453
479
|
|
|
454
|
-
# Get the field value
|
|
455
|
-
value =
|
|
480
|
+
# Get the field value using centralized field extraction
|
|
481
|
+
value = get_field_value_for_db(obj, field_name, obj.__class__)
|
|
456
482
|
|
|
457
483
|
# Handle NULL values specially for ForeignKey fields
|
|
458
484
|
if is_fk and value is None:
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utilities for field value extraction and normalization.
|
|
3
|
+
|
|
4
|
+
Single source of truth for converting model instance field values
|
|
5
|
+
to their database representation. This eliminates duplicate FK handling
|
|
6
|
+
logic scattered across multiple components.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from django.db.models import ForeignKey
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_field_value_for_db(obj, field_name, model_cls=None):
|
|
17
|
+
"""
|
|
18
|
+
Get a field value from an object in its database-ready form.
|
|
19
|
+
|
|
20
|
+
For regular fields: returns the field value as-is
|
|
21
|
+
For FK fields: returns the PK (integer) instead of the related object
|
|
22
|
+
|
|
23
|
+
This ensures consistent handling across all operations (upsert classification,
|
|
24
|
+
bulk create, bulk update, etc.)
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
obj: Model instance
|
|
28
|
+
field_name: Name of the field to extract
|
|
29
|
+
model_cls: Model class (optional, will infer from obj if not provided)
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Database-ready value (FK as integer, regular fields as-is)
|
|
33
|
+
"""
|
|
34
|
+
if model_cls is None:
|
|
35
|
+
model_cls = obj.__class__
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
field = model_cls._meta.get_field(field_name)
|
|
39
|
+
except Exception: # noqa: BLE001
|
|
40
|
+
# Field doesn't exist - just get attribute
|
|
41
|
+
return getattr(obj, field_name, None)
|
|
42
|
+
|
|
43
|
+
# Check if it's a ForeignKey
|
|
44
|
+
if isinstance(field, ForeignKey):
|
|
45
|
+
# For FK fields, always use attname (e.g., 'business_id' not 'business')
|
|
46
|
+
# This gets the raw FK ID value from the database field
|
|
47
|
+
return getattr(obj, field.attname, None)
|
|
48
|
+
|
|
49
|
+
# Regular field - get normally
|
|
50
|
+
return getattr(obj, field_name, None)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_field_values_for_db(obj, field_names, model_cls=None):
|
|
54
|
+
"""
|
|
55
|
+
Get multiple field values from an object in database-ready form.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
obj: Model instance
|
|
59
|
+
field_names: List of field names
|
|
60
|
+
model_cls: Model class (optional)
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Dict of {field_name: db_value}
|
|
64
|
+
"""
|
|
65
|
+
if model_cls is None:
|
|
66
|
+
model_cls = obj.__class__
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
field_name: get_field_value_for_db(obj, field_name, model_cls)
|
|
70
|
+
for field_name in field_names
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def normalize_field_name_to_db(field_name, model_cls):
|
|
75
|
+
"""
|
|
76
|
+
Normalize a field name to its database column name.
|
|
77
|
+
|
|
78
|
+
For FK fields referenced by relationship name, returns the attname (e.g., 'business' -> 'business_id')
|
|
79
|
+
For regular fields, returns as-is.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
field_name: Field name (can be 'business' or 'business_id')
|
|
83
|
+
model_cls: Model class
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Database column name
|
|
87
|
+
"""
|
|
88
|
+
try:
|
|
89
|
+
field = model_cls._meta.get_field(field_name)
|
|
90
|
+
if isinstance(field, ForeignKey):
|
|
91
|
+
return field.attname # Returns 'business_id' for 'business' field
|
|
92
|
+
return field_name
|
|
93
|
+
except Exception: # noqa: BLE001
|
|
94
|
+
return field_name
|
|
@@ -11,6 +11,8 @@ import logging
|
|
|
11
11
|
|
|
12
12
|
from django.db.models import AutoField
|
|
13
13
|
|
|
14
|
+
from django_bulk_hooks.operations.field_utils import get_field_value_for_db
|
|
15
|
+
|
|
14
16
|
logger = logging.getLogger(__name__)
|
|
15
17
|
|
|
16
18
|
|
|
@@ -354,19 +356,13 @@ class MTIHandler:
|
|
|
354
356
|
"""
|
|
355
357
|
parent_obj = parent_model()
|
|
356
358
|
|
|
357
|
-
# Copy field values from source
|
|
359
|
+
# Copy field values from source using centralized field extraction
|
|
358
360
|
for field in parent_model._meta.local_fields:
|
|
359
361
|
if hasattr(source_obj, field.name):
|
|
360
|
-
|
|
362
|
+
# Use centralized field value extraction for consistent FK handling
|
|
363
|
+
value = get_field_value_for_db(source_obj, field.name, source_obj.__class__)
|
|
361
364
|
if value is not None:
|
|
362
|
-
|
|
363
|
-
# Handle FK fields
|
|
364
|
-
if hasattr(value, "pk") and value.pk is not None:
|
|
365
|
-
setattr(parent_obj, field.attname, value.pk)
|
|
366
|
-
else:
|
|
367
|
-
setattr(parent_obj, field.attname, value)
|
|
368
|
-
else:
|
|
369
|
-
setattr(parent_obj, field.name, value)
|
|
365
|
+
setattr(parent_obj, field.attname, value)
|
|
370
366
|
|
|
371
367
|
# Link to parent if exists
|
|
372
368
|
if current_parent is not None:
|
|
@@ -419,15 +415,10 @@ class MTIHandler:
|
|
|
419
415
|
continue
|
|
420
416
|
|
|
421
417
|
if hasattr(source_obj, field.name):
|
|
422
|
-
|
|
418
|
+
# Use centralized field value extraction for consistent FK handling
|
|
419
|
+
value = get_field_value_for_db(source_obj, field.name, source_obj.__class__)
|
|
423
420
|
if value is not None:
|
|
424
|
-
|
|
425
|
-
if hasattr(value, "pk") and value.pk is not None:
|
|
426
|
-
setattr(child_obj, field.attname, value.pk)
|
|
427
|
-
else:
|
|
428
|
-
setattr(child_obj, field.attname, value)
|
|
429
|
-
else:
|
|
430
|
-
setattr(child_obj, field.name, value)
|
|
421
|
+
setattr(child_obj, field.attname, value)
|
|
431
422
|
|
|
432
423
|
# Copy object state
|
|
433
424
|
if hasattr(source_obj, "_state") and hasattr(child_obj, "_state"):
|
|
@@ -11,6 +11,8 @@ import logging
|
|
|
11
11
|
|
|
12
12
|
from django.db.models import Q
|
|
13
13
|
|
|
14
|
+
from django_bulk_hooks.operations.field_utils import get_field_value_for_db
|
|
15
|
+
|
|
14
16
|
logger = logging.getLogger(__name__)
|
|
15
17
|
|
|
16
18
|
|
|
@@ -61,17 +63,22 @@ class RecordClassifier:
|
|
|
61
63
|
for obj in objs:
|
|
62
64
|
# Build lookup dict for this object's unique fields
|
|
63
65
|
lookup = {}
|
|
66
|
+
normalized_values = []
|
|
67
|
+
|
|
64
68
|
for field_name in unique_fields:
|
|
65
|
-
value
|
|
69
|
+
# Use centralized field value extraction for consistent FK handling
|
|
70
|
+
value = get_field_value_for_db(obj, field_name, query_model)
|
|
66
71
|
if value is None:
|
|
67
72
|
# Can't match on None values
|
|
68
73
|
break
|
|
69
74
|
lookup[field_name] = value
|
|
75
|
+
normalized_values.append(value)
|
|
70
76
|
else:
|
|
71
77
|
# All unique fields have values, add to query
|
|
72
78
|
if lookup:
|
|
73
79
|
queries.append(Q(**lookup))
|
|
74
|
-
|
|
80
|
+
# Store normalized values for comparison with database results
|
|
81
|
+
obj_to_unique_values[id(obj)] = tuple(normalized_values)
|
|
75
82
|
|
|
76
83
|
if not queries:
|
|
77
84
|
return set(), {}
|
|
@@ -13,14 +13,15 @@ django_bulk_hooks/manager.py,sha256=3mFzB0ZzHHeXWdKGObZD_H0NlskHJc8uYBF69KKdAXU,
|
|
|
13
13
|
django_bulk_hooks/models.py,sha256=4Vvi2LiGP0g4j08a5liqBROfsO8Wd_ermBoyjKwfrPU,2512
|
|
14
14
|
django_bulk_hooks/operations/__init__.py,sha256=BtJYjmRhe_sScivLsniDaZmBkm0ZLvcmzXFKL7QY2Xg,550
|
|
15
15
|
django_bulk_hooks/operations/analyzer.py,sha256=wAG8sAG9NwfwNqG9z81VfGR7AANDzRmMGE_o82MWji4,10689
|
|
16
|
-
django_bulk_hooks/operations/bulk_executor.py,sha256=
|
|
16
|
+
django_bulk_hooks/operations/bulk_executor.py,sha256=F_nXtqB2ZRuAEZtSzqtKedqH_TDkNQDPLVmVUT8jsPQ,24794
|
|
17
17
|
django_bulk_hooks/operations/coordinator.py,sha256=iGavJLqe3eYRqFay8cMn6muwyRYzQo-HFGphsS5hL6g,30799
|
|
18
|
-
django_bulk_hooks/operations/
|
|
18
|
+
django_bulk_hooks/operations/field_utils.py,sha256=Tvr5bcZLG8imH-r2S85oui1Cbw6hGv3VtuIMn4OvsU4,2895
|
|
19
|
+
django_bulk_hooks/operations/mti_handler.py,sha256=XklnU3l9c44Lu32WwS9FmS34XqRw-41XuEdIkg7Ws1U,20461
|
|
19
20
|
django_bulk_hooks/operations/mti_plans.py,sha256=7STQ2oA2ZT8cEG3-t-6xciRAdf7OeSf0gRLXR_BRG-Q,3363
|
|
20
|
-
django_bulk_hooks/operations/record_classifier.py,sha256=
|
|
21
|
+
django_bulk_hooks/operations/record_classifier.py,sha256=kqML4aO11X9K3SSJ5DUlUukwI172j_Tk12Kr77ee8q8,7065
|
|
21
22
|
django_bulk_hooks/queryset.py,sha256=aQitlbexcVnmeAdc0jtO3hci39p4QEu4srQPEzozy5s,5546
|
|
22
23
|
django_bulk_hooks/registry.py,sha256=uum5jhGI3TPaoiXuA1MdBdu4gbE3rQGGwQ5YDjiMcjk,7949
|
|
23
|
-
django_bulk_hooks-0.2.
|
|
24
|
-
django_bulk_hooks-0.2.
|
|
25
|
-
django_bulk_hooks-0.2.
|
|
26
|
-
django_bulk_hooks-0.2.
|
|
24
|
+
django_bulk_hooks-0.2.51.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
25
|
+
django_bulk_hooks-0.2.51.dist-info/METADATA,sha256=4aW_8Vv9jEwSfHVdsZWiX9YehJgwGWGBSpS1ADmgI3E,9265
|
|
26
|
+
django_bulk_hooks-0.2.51.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
27
|
+
django_bulk_hooks-0.2.51.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|