django-bulk-hooks 0.2.9__py3-none-any.whl → 0.2.11__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/decorators.py +55 -7
- django_bulk_hooks/dispatcher.py +13 -2
- django_bulk_hooks/operations/analyzer.py +1 -63
- django_bulk_hooks/operations/coordinator.py +39 -0
- {django_bulk_hooks-0.2.9.dist-info → django_bulk_hooks-0.2.11.dist-info}/METADATA +1 -1
- {django_bulk_hooks-0.2.9.dist-info → django_bulk_hooks-0.2.11.dist-info}/RECORD +8 -9
- django_bulk_hooks/debug_utils.py +0 -145
- {django_bulk_hooks-0.2.9.dist-info → django_bulk_hooks-0.2.11.dist-info}/LICENSE +0 -0
- {django_bulk_hooks-0.2.9.dist-info → django_bulk_hooks-0.2.11.dist-info}/WHEEL +0 -0
django_bulk_hooks/decorators.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
import logging
|
|
2
3
|
from functools import wraps
|
|
3
4
|
|
|
4
5
|
from django.core.exceptions import FieldDoesNotExist
|
|
@@ -6,6 +7,8 @@ from django.core.exceptions import FieldDoesNotExist
|
|
|
6
7
|
from django_bulk_hooks.enums import DEFAULT_PRIORITY
|
|
7
8
|
from django_bulk_hooks.registry import register_hook
|
|
8
9
|
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
9
12
|
|
|
10
13
|
def hook(event, *, model, condition=None, priority=DEFAULT_PRIORITY):
|
|
11
14
|
"""
|
|
@@ -35,7 +38,7 @@ def select_related(*related_fields):
|
|
|
35
38
|
def decorator(func):
|
|
36
39
|
sig = inspect.signature(func)
|
|
37
40
|
|
|
38
|
-
def preload_related(records, *, model_cls=None):
|
|
41
|
+
def preload_related(records, *, model_cls=None, skip_fields=None):
|
|
39
42
|
if not isinstance(records, list):
|
|
40
43
|
raise TypeError(
|
|
41
44
|
f"@select_related expects a list of model instances, got {type(records)}"
|
|
@@ -47,6 +50,9 @@ def select_related(*related_fields):
|
|
|
47
50
|
if model_cls is None:
|
|
48
51
|
model_cls = records[0].__class__
|
|
49
52
|
|
|
53
|
+
if skip_fields is None:
|
|
54
|
+
skip_fields = set()
|
|
55
|
+
|
|
50
56
|
# Validate field notation upfront
|
|
51
57
|
for field in related_fields:
|
|
52
58
|
if "." in field:
|
|
@@ -161,6 +167,10 @@ def select_related(*related_fields):
|
|
|
161
167
|
continue
|
|
162
168
|
|
|
163
169
|
for field in related_fields:
|
|
170
|
+
# Skip preloading if this relationship conflicts with FK field being updated
|
|
171
|
+
if field in skip_fields:
|
|
172
|
+
continue
|
|
173
|
+
|
|
164
174
|
if fields_cache is not None and field in fields_cache:
|
|
165
175
|
continue
|
|
166
176
|
|
|
@@ -179,6 +189,10 @@ def select_related(*related_fields):
|
|
|
179
189
|
continue
|
|
180
190
|
|
|
181
191
|
for field_name, relation_field in direct_relation_fields.items():
|
|
192
|
+
# Skip preloading if this relationship conflicts with FK field being updated
|
|
193
|
+
if field_name in skip_fields:
|
|
194
|
+
continue
|
|
195
|
+
|
|
182
196
|
if fields_cache is not None and field_name in fields_cache:
|
|
183
197
|
continue
|
|
184
198
|
|
|
@@ -198,6 +212,12 @@ def select_related(*related_fields):
|
|
|
198
212
|
if fields_cache is not None:
|
|
199
213
|
fields_cache[field_name] = rel_obj
|
|
200
214
|
|
|
215
|
+
def preload_with_skip_fields(records, *, model_cls=None, skip_fields=None):
|
|
216
|
+
"""Wrapper that applies skip_fields logic to the preload function"""
|
|
217
|
+
if skip_fields is None:
|
|
218
|
+
skip_fields = set()
|
|
219
|
+
return preload_related(records, model_cls=model_cls, skip_fields=skip_fields)
|
|
220
|
+
|
|
201
221
|
@wraps(func)
|
|
202
222
|
def wrapper(*args, **kwargs):
|
|
203
223
|
bound = sig.bind_partial(*args, **kwargs)
|
|
@@ -210,13 +230,27 @@ def select_related(*related_fields):
|
|
|
210
230
|
|
|
211
231
|
new_records = bound.arguments["new_records"]
|
|
212
232
|
|
|
213
|
-
|
|
233
|
+
if not isinstance(new_records, list):
|
|
234
|
+
raise TypeError(
|
|
235
|
+
f"@select_related expects a list of model instances, got {type(new_records)}"
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if not new_records:
|
|
239
|
+
# Empty list, nothing to preload
|
|
240
|
+
return func(*args, **kwargs)
|
|
214
241
|
|
|
215
|
-
|
|
242
|
+
# Validate field notation upfront (same as in preload_related)
|
|
243
|
+
for field in related_fields:
|
|
244
|
+
if "." in field:
|
|
245
|
+
raise ValueError(
|
|
246
|
+
f"Invalid field notation '{field}'. Use Django ORM __ notation (e.g., 'parent__field')"
|
|
247
|
+
)
|
|
216
248
|
|
|
217
|
-
|
|
249
|
+
# Don't preload here - let the dispatcher handle it
|
|
250
|
+
# The dispatcher will call the preload function with skip_fields
|
|
251
|
+
return func(*args, **kwargs)
|
|
218
252
|
|
|
219
|
-
wrapper._select_related_preload =
|
|
253
|
+
wrapper._select_related_preload = preload_with_skip_fields
|
|
220
254
|
wrapper._select_related_fields = related_fields
|
|
221
255
|
|
|
222
256
|
return wrapper
|
|
@@ -241,8 +275,22 @@ def bulk_hook(model_cls, event, when=None, priority=None):
|
|
|
241
275
|
def __init__(self):
|
|
242
276
|
self.func = func
|
|
243
277
|
|
|
244
|
-
def handle(self, new_records=None, old_records=None, **kwargs):
|
|
245
|
-
|
|
278
|
+
def handle(self, changeset=None, new_records=None, old_records=None, **kwargs):
|
|
279
|
+
# Support both old and new hook signatures for backward compatibility
|
|
280
|
+
# Old signature: def hook(self, new_records, old_records, **kwargs)
|
|
281
|
+
# New signature: def hook(self, changeset, new_records, old_records, **kwargs)
|
|
282
|
+
|
|
283
|
+
# Check function signature to determine which format to use
|
|
284
|
+
import inspect
|
|
285
|
+
sig = inspect.signature(func)
|
|
286
|
+
params = list(sig.parameters.keys())
|
|
287
|
+
|
|
288
|
+
if 'changeset' in params:
|
|
289
|
+
# New signature with changeset
|
|
290
|
+
return self.func(changeset, new_records, old_records, **kwargs)
|
|
291
|
+
else:
|
|
292
|
+
# Old signature without changeset
|
|
293
|
+
return self.func(new_records, old_records, **kwargs)
|
|
246
294
|
|
|
247
295
|
# Register the hook using the registry
|
|
248
296
|
register_hook(
|
django_bulk_hooks/dispatcher.py
CHANGED
|
@@ -108,7 +108,9 @@ class HookDispatcher:
|
|
|
108
108
|
return
|
|
109
109
|
|
|
110
110
|
# Execute hooks in priority order
|
|
111
|
+
logger.info(f"🔥 HOOKS: Executing {len(hooks)} hooks for {changeset.model_cls.__name__}.{event}")
|
|
111
112
|
for handler_cls, method_name, condition, priority in hooks:
|
|
113
|
+
logger.info(f" → {handler_cls.__name__}.{method_name} (priority={priority})")
|
|
112
114
|
self._execute_hook(handler_cls, method_name, condition, changeset)
|
|
113
115
|
|
|
114
116
|
def _execute_hook(self, handler_cls, method_name, condition, changeset):
|
|
@@ -159,6 +161,9 @@ class HookDispatcher:
|
|
|
159
161
|
try:
|
|
160
162
|
model_cls_override = getattr(handler, "model_cls", None)
|
|
161
163
|
|
|
164
|
+
# Get FK fields being updated to avoid preloading conflicting relationships
|
|
165
|
+
skip_fields = changeset.operation_meta.get('fk_fields_being_updated', set())
|
|
166
|
+
|
|
162
167
|
# Preload for new_records
|
|
163
168
|
if filtered_changeset.new_records:
|
|
164
169
|
logger.debug(
|
|
@@ -166,7 +171,9 @@ class HookDispatcher:
|
|
|
166
171
|
f"new_records for {handler_cls.__name__}.{method_name}"
|
|
167
172
|
)
|
|
168
173
|
preload_func(
|
|
169
|
-
filtered_changeset.new_records,
|
|
174
|
+
filtered_changeset.new_records,
|
|
175
|
+
model_cls=model_cls_override,
|
|
176
|
+
skip_fields=skip_fields
|
|
170
177
|
)
|
|
171
178
|
|
|
172
179
|
# Also preload for old_records (for conditions that check previous values)
|
|
@@ -176,7 +183,9 @@ class HookDispatcher:
|
|
|
176
183
|
f"old_records for {handler_cls.__name__}.{method_name}"
|
|
177
184
|
)
|
|
178
185
|
preload_func(
|
|
179
|
-
filtered_changeset.old_records,
|
|
186
|
+
filtered_changeset.old_records,
|
|
187
|
+
model_cls=model_cls_override,
|
|
188
|
+
skip_fields=skip_fields
|
|
180
189
|
)
|
|
181
190
|
except Exception:
|
|
182
191
|
logger.debug(
|
|
@@ -197,12 +206,14 @@ class HookDispatcher:
|
|
|
197
206
|
# New hooks that do use changeset: def hook(self, changeset, new_records, old_records, **kwargs)
|
|
198
207
|
#
|
|
199
208
|
# This is standard Python framework design (see Django signals, Flask hooks, etc.)
|
|
209
|
+
logger.info(f" 🚀 Executing: {handler_cls.__name__}.{method_name}")
|
|
200
210
|
try:
|
|
201
211
|
method(
|
|
202
212
|
changeset=filtered_changeset,
|
|
203
213
|
new_records=filtered_changeset.new_records,
|
|
204
214
|
old_records=filtered_changeset.old_records,
|
|
205
215
|
)
|
|
216
|
+
logger.info(f" ✅ Completed: {handler_cls.__name__}.{method_name}")
|
|
206
217
|
except Exception as e:
|
|
207
218
|
# Fail-fast: re-raise to rollback transaction
|
|
208
219
|
logger.error(
|
|
@@ -257,10 +257,6 @@ class ModelAnalyzer:
|
|
|
257
257
|
F() expressions, Subquery, Case, etc.) into concrete values and applies
|
|
258
258
|
them to the instances.
|
|
259
259
|
|
|
260
|
-
CRITICAL: When setting FK fields by their attname (e.g., business_id),
|
|
261
|
-
we must manually clear the relationship cache (e.g., business) to match
|
|
262
|
-
Django's ForeignKey descriptor behavior.
|
|
263
|
-
|
|
264
260
|
Args:
|
|
265
261
|
instances: List of model instances to update
|
|
266
262
|
update_kwargs: Dict of {field_name: value_or_expression}
|
|
@@ -274,66 +270,8 @@ class ModelAnalyzer:
|
|
|
274
270
|
fields_updated = list(update_kwargs.keys())
|
|
275
271
|
|
|
276
272
|
for field_name, value in update_kwargs.items():
|
|
277
|
-
# Determine if this is a FK field being set by its attname
|
|
278
|
-
field_info = self._get_fk_field_info(field_name)
|
|
279
|
-
|
|
280
273
|
for instance in instances:
|
|
281
274
|
resolved_value = self.resolve_expression(field_name, value, instance)
|
|
282
275
|
setattr(instance, field_name, resolved_value)
|
|
283
|
-
|
|
284
|
-
# Clear relationship cache when FK field is set directly
|
|
285
|
-
# This replicates Django's ForeignKey descriptor behavior
|
|
286
|
-
if field_info and field_info['is_fk_attname']:
|
|
287
|
-
self._clear_fk_cache(instance, field_info['accessor_name'])
|
|
288
|
-
|
|
289
|
-
return fields_updated
|
|
290
|
-
|
|
291
|
-
def _get_fk_field_info(self, field_name):
|
|
292
|
-
"""
|
|
293
|
-
Get information about a FK field if field_name is a FK attname.
|
|
294
|
-
|
|
295
|
-
Args:
|
|
296
|
-
field_name: Field name to check
|
|
297
|
-
|
|
298
|
-
Returns:
|
|
299
|
-
Dict with FK info or None if not a FK field
|
|
300
|
-
"""
|
|
301
|
-
try:
|
|
302
|
-
# Check all fields to find if this is a FK attname
|
|
303
|
-
for field in self.model_cls._meta.get_fields():
|
|
304
|
-
if (field.is_relation and
|
|
305
|
-
not field.many_to_many and
|
|
306
|
-
not field.one_to_many and
|
|
307
|
-
hasattr(field, 'attname') and
|
|
308
|
-
field.attname == field_name):
|
|
309
|
-
# This is a FK field being set by its attname (e.g., business_id)
|
|
310
|
-
return {
|
|
311
|
-
'is_fk_attname': True,
|
|
312
|
-
'accessor_name': field.name, # e.g., 'business'
|
|
313
|
-
'field': field
|
|
314
|
-
}
|
|
315
|
-
except Exception as e:
|
|
316
|
-
logger.debug(f"Error checking FK field info for {field_name}: {e}")
|
|
317
|
-
|
|
318
|
-
return None
|
|
319
|
-
|
|
320
|
-
def _clear_fk_cache(self, instance, accessor_name):
|
|
321
|
-
"""
|
|
322
|
-
Clear cached relationship when FK field is set directly.
|
|
323
276
|
|
|
324
|
-
|
|
325
|
-
when you set a FK field, Django clears the cached related object.
|
|
326
|
-
|
|
327
|
-
Args:
|
|
328
|
-
instance: Model instance
|
|
329
|
-
accessor_name: Name of the relationship accessor (e.g., 'business')
|
|
330
|
-
"""
|
|
331
|
-
try:
|
|
332
|
-
if hasattr(instance, '_state') and hasattr(instance._state, 'fields_cache'):
|
|
333
|
-
instance._state.fields_cache.pop(accessor_name, None)
|
|
334
|
-
logger.debug(
|
|
335
|
-
f"Cleared FK cache for '{accessor_name}' on {self.model_cls.__name__}"
|
|
336
|
-
)
|
|
337
|
-
except Exception as e:
|
|
338
|
-
# Don't fail the operation, just log
|
|
339
|
-
logger.debug(f"Could not clear FK cache for {accessor_name}: {e}")
|
|
277
|
+
return fields_updated
|
|
@@ -276,6 +276,9 @@ class BulkOperationCoordinator:
|
|
|
276
276
|
# Fetch old records for comparison (single bulk query)
|
|
277
277
|
old_records_map = self.analyzer.fetch_old_records_map(instances)
|
|
278
278
|
|
|
279
|
+
# Detect FK fields being updated to prevent @select_related conflicts
|
|
280
|
+
fk_fields_being_updated = self._get_fk_fields_being_updated(update_kwargs)
|
|
281
|
+
|
|
279
282
|
# Build changeset for VALIDATE and BEFORE hooks
|
|
280
283
|
# instances now have the "intended" values from update_kwargs
|
|
281
284
|
changeset = build_changeset_for_update(
|
|
@@ -285,6 +288,10 @@ class BulkOperationCoordinator:
|
|
|
285
288
|
old_records_map=old_records_map,
|
|
286
289
|
)
|
|
287
290
|
|
|
291
|
+
# Add FK field info to changeset meta for dispatcher to use
|
|
292
|
+
if fk_fields_being_updated:
|
|
293
|
+
changeset.operation_meta['fk_fields_being_updated'] = fk_fields_being_updated
|
|
294
|
+
|
|
288
295
|
# Execute VALIDATE and BEFORE hooks
|
|
289
296
|
# Hooks can now modify the instances and changes will persist
|
|
290
297
|
if not bypass_validation:
|
|
@@ -470,3 +477,35 @@ class BulkOperationCoordinator:
|
|
|
470
477
|
self.dispatcher.dispatch(model_changeset, f"after_{event_prefix}", bypass_hooks=False)
|
|
471
478
|
|
|
472
479
|
return result
|
|
480
|
+
|
|
481
|
+
def _get_fk_fields_being_updated(self, update_kwargs):
|
|
482
|
+
"""
|
|
483
|
+
Get the relationship names for FK fields being updated.
|
|
484
|
+
|
|
485
|
+
This helps @select_related avoid preloading relationships that are
|
|
486
|
+
being modified, which can cause cache conflicts.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
update_kwargs: Dict of fields being updated
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Set of relationship names (e.g., {'business'}) for FK fields being updated
|
|
493
|
+
"""
|
|
494
|
+
fk_relationships = set()
|
|
495
|
+
|
|
496
|
+
for field_name in update_kwargs.keys():
|
|
497
|
+
try:
|
|
498
|
+
field = self.model_cls._meta.get_field(field_name)
|
|
499
|
+
if (field.is_relation and
|
|
500
|
+
not field.many_to_many and
|
|
501
|
+
not field.one_to_many and
|
|
502
|
+
hasattr(field, 'attname') and
|
|
503
|
+
field.attname == field_name):
|
|
504
|
+
# This is a FK field being updated by its attname (e.g., business_id)
|
|
505
|
+
# Add the relationship name (e.g., 'business') to skip list
|
|
506
|
+
fk_relationships.add(field.name)
|
|
507
|
+
except Exception:
|
|
508
|
+
# If field lookup fails, skip it
|
|
509
|
+
continue
|
|
510
|
+
|
|
511
|
+
return fk_relationships
|
|
@@ -3,9 +3,8 @@ django_bulk_hooks/changeset.py,sha256=WALeiWDcjOBNdCKeidVKOPKAySKj9ZOvUJ-kWaVZYh
|
|
|
3
3
|
django_bulk_hooks/conditions.py,sha256=qtGjToKXC8FPUPK31Mib-GMzc9GSdrH90M2pT3CIsh8,8111
|
|
4
4
|
django_bulk_hooks/constants.py,sha256=PxpEETaO6gdENcTPoXS586lerGKVP3nmjpDvOkmhYxI,509
|
|
5
5
|
django_bulk_hooks/context.py,sha256=mqaC5-yESDTA5ruI7fuXlt8qSgKuOFp0mjq7h1-4HdQ,1926
|
|
6
|
-
django_bulk_hooks/
|
|
7
|
-
django_bulk_hooks/
|
|
8
|
-
django_bulk_hooks/dispatcher.py,sha256=L5_hSqENuKXDftJOdMetfjdZkiakUgkheqU8HpWKaOI,8214
|
|
6
|
+
django_bulk_hooks/decorators.py,sha256=hc8MSG5XXEiT5kgsf4Opzpj8jAb-OYqcvssuZxCIncQ,11894
|
|
7
|
+
django_bulk_hooks/dispatcher.py,sha256=k3ndhY8e2N2nIHn22hFrCsDd3U5RVbPQtQ6Xf9E8UQE,8923
|
|
9
8
|
django_bulk_hooks/enums.py,sha256=Zo8_tJzuzZ2IKfVc7gZ-0tWPT8q1QhqZbAyoh9ZVJbs,381
|
|
10
9
|
django_bulk_hooks/factory.py,sha256=JmjQiJPfAnytXrO6r6qOadX5yX0-sfpbZ9V8nwX3MAg,20013
|
|
11
10
|
django_bulk_hooks/handler.py,sha256=2-k0GPWGSQ6acfvV0qJgDH8aa0z51DqdpX5vSJ6Uawk,4759
|
|
@@ -13,14 +12,14 @@ django_bulk_hooks/helpers.py,sha256=Yopvl588VbKOi2kHEsQcEcI5jw5jiNA2MuF6Ce1VP0c,
|
|
|
13
12
|
django_bulk_hooks/manager.py,sha256=3mFzB0ZzHHeXWdKGObZD_H0NlskHJc8uYBF69KKdAXU,4068
|
|
14
13
|
django_bulk_hooks/models.py,sha256=62tn5wL55EjJVOsZofMluhEJB8bH7CzBvH0vd214_RY,2570
|
|
15
14
|
django_bulk_hooks/operations/__init__.py,sha256=5L5NnwiFw8Yn5WO6-38eGdCYBkA0URpwyDcAdeYfc5w,550
|
|
16
|
-
django_bulk_hooks/operations/analyzer.py,sha256=
|
|
15
|
+
django_bulk_hooks/operations/analyzer.py,sha256=s6FM53ho1raPdKU-VjjW0SWymXyrJe0I_Wu8XsXFdSY,9065
|
|
17
16
|
django_bulk_hooks/operations/bulk_executor.py,sha256=7VJgeTFcMQ9ZELvCV6WR6udUPJNL6Kf-w9iEva6pIPA,18271
|
|
18
|
-
django_bulk_hooks/operations/coordinator.py,sha256=
|
|
17
|
+
django_bulk_hooks/operations/coordinator.py,sha256=tCQA0yfnt1bh8hLR_g_WlZc2tRpnWDbTb9aWKaAfWmo,18174
|
|
19
18
|
django_bulk_hooks/operations/mti_handler.py,sha256=eIH-tImMqcWR5lLQr6Ca-HeVYta-UkXk5X5fcpS885Y,18245
|
|
20
19
|
django_bulk_hooks/operations/mti_plans.py,sha256=fHUYbrUAHq8UXqxgAD43oHdTxOnEkmpxoOD4Qrzfqk8,2878
|
|
21
20
|
django_bulk_hooks/queryset.py,sha256=ody4MXrRREL27Ts2ey1UpS0tb5Dxnw-6kN3unxPQ3zY,5860
|
|
22
21
|
django_bulk_hooks/registry.py,sha256=UPerNhtVz_9tKZqrYSZD2LhjAcs4F6hVUuk8L5oOeHc,8821
|
|
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-0.2.11.dist-info/LICENSE,sha256=dguKIcbDGeZD-vXWdLyErPUALYOvtX_fO4Zjhq481uk,1088
|
|
23
|
+
django_bulk_hooks-0.2.11.dist-info/METADATA,sha256=XjBb-_Q9gbnDEim8E7qQ4MY-iUUourgH8nNEkdFvHCI,9265
|
|
24
|
+
django_bulk_hooks-0.2.11.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
25
|
+
django_bulk_hooks-0.2.11.dist-info/RECORD,,
|
django_bulk_hooks/debug_utils.py
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Debug utilities for tracking N+1 queries and database performance.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
import time
|
|
7
|
-
from functools import wraps
|
|
8
|
-
from django.db import connection
|
|
9
|
-
from django.conf import settings
|
|
10
|
-
|
|
11
|
-
logger = logging.getLogger(__name__)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def track_queries(func):
|
|
15
|
-
"""
|
|
16
|
-
Decorator to track database queries during function execution.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
@wraps(func)
|
|
20
|
-
def wrapper(*args, **kwargs):
|
|
21
|
-
# Reset query count
|
|
22
|
-
initial_queries = len(connection.queries)
|
|
23
|
-
initial_time = time.time()
|
|
24
|
-
|
|
25
|
-
logger.debug(
|
|
26
|
-
f"QUERY DEBUG: Starting {func.__name__} - initial query count: {initial_queries}"
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
try:
|
|
30
|
-
result = func(*args, **kwargs)
|
|
31
|
-
|
|
32
|
-
final_queries = len(connection.queries)
|
|
33
|
-
final_time = time.time()
|
|
34
|
-
query_count = final_queries - initial_queries
|
|
35
|
-
duration = final_time - initial_time
|
|
36
|
-
|
|
37
|
-
logger.debug(
|
|
38
|
-
f"QUERY DEBUG: Completed {func.__name__} - queries executed: {query_count}, duration: {duration:.4f}s"
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
# Log all queries executed during this function
|
|
42
|
-
if query_count > 0:
|
|
43
|
-
logger.debug(f"QUERY DEBUG: Queries executed in {func.__name__}:")
|
|
44
|
-
for i, query in enumerate(connection.queries[initial_queries:], 1):
|
|
45
|
-
logger.debug(
|
|
46
|
-
f"QUERY DEBUG: {i}. {query['sql'][:100]}... (time: {query['time']})"
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
return result
|
|
50
|
-
|
|
51
|
-
except Exception as e:
|
|
52
|
-
final_queries = len(connection.queries)
|
|
53
|
-
query_count = final_queries - initial_queries
|
|
54
|
-
logger.debug(
|
|
55
|
-
f"QUERY DEBUG: Exception in {func.__name__} - queries executed: {query_count}"
|
|
56
|
-
)
|
|
57
|
-
raise
|
|
58
|
-
|
|
59
|
-
return wrapper
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def log_query_count(context=""):
|
|
63
|
-
"""
|
|
64
|
-
Log the current query count with optional context.
|
|
65
|
-
"""
|
|
66
|
-
query_count = len(connection.queries)
|
|
67
|
-
logger.debug(f"QUERY DEBUG: Query count at {context}: {query_count}")
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def log_recent_queries(count=5, context=""):
|
|
71
|
-
"""
|
|
72
|
-
Log the most recent database queries.
|
|
73
|
-
"""
|
|
74
|
-
recent_queries = connection.queries[-count:] if connection.queries else []
|
|
75
|
-
logger.debug(f"QUERY DEBUG: Recent {len(recent_queries)} queries at {context}:")
|
|
76
|
-
for i, query in enumerate(recent_queries, 1):
|
|
77
|
-
logger.debug(
|
|
78
|
-
f"QUERY DEBUG: {i}. {query['sql'][:100]}... (time: {query['time']})"
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
class QueryTracker:
|
|
83
|
-
"""
|
|
84
|
-
Context manager for tracking database queries.
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
def __init__(self, context_name="QueryTracker"):
|
|
88
|
-
self.context_name = context_name
|
|
89
|
-
self.initial_queries = 0
|
|
90
|
-
self.start_time = 0
|
|
91
|
-
|
|
92
|
-
def __enter__(self):
|
|
93
|
-
self.initial_queries = len(connection.queries)
|
|
94
|
-
self.start_time = time.time()
|
|
95
|
-
logger.debug(
|
|
96
|
-
f"QUERY DEBUG: Starting {self.context_name} - initial query count: {self.initial_queries}"
|
|
97
|
-
)
|
|
98
|
-
return self
|
|
99
|
-
|
|
100
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
101
|
-
final_queries = len(connection.queries)
|
|
102
|
-
final_time = time.time()
|
|
103
|
-
query_count = final_queries - self.initial_queries
|
|
104
|
-
duration = final_time - self.start_time
|
|
105
|
-
|
|
106
|
-
logger.debug(
|
|
107
|
-
f"QUERY DEBUG: Completed {self.context_name} - queries executed: {query_count}, duration: {duration:.4f}s"
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
if query_count > 0:
|
|
111
|
-
logger.debug(f"QUERY DEBUG: Queries executed in {self.context_name}:")
|
|
112
|
-
for i, query in enumerate(connection.queries[self.initial_queries :], 1):
|
|
113
|
-
logger.debug(
|
|
114
|
-
f"QUERY DEBUG: {i}. {query['sql'][:100]}... (time: {query['time']})"
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
return False # Don't suppress exceptions
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def enable_django_query_logging():
|
|
121
|
-
"""
|
|
122
|
-
Enable Django's built-in query logging.
|
|
123
|
-
"""
|
|
124
|
-
if not settings.DEBUG:
|
|
125
|
-
logger.warning("Django query logging can only be enabled in DEBUG mode")
|
|
126
|
-
return
|
|
127
|
-
|
|
128
|
-
# Enable query logging
|
|
129
|
-
settings.LOGGING = {
|
|
130
|
-
"version": 1,
|
|
131
|
-
"disable_existing_loggers": False,
|
|
132
|
-
"handlers": {
|
|
133
|
-
"console": {
|
|
134
|
-
"class": "logging.StreamHandler",
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
"loggers": {
|
|
138
|
-
"django.db.backends": {
|
|
139
|
-
"level": "DEBUG",
|
|
140
|
-
"handlers": ["console"],
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
logger.info("Django query logging enabled")
|
|
File without changes
|
|
File without changes
|