statezero 0.1.0b1__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.
Files changed (45) hide show
  1. statezero/__init__.py +0 -0
  2. statezero/adaptors/__init__.py +0 -0
  3. statezero/adaptors/django/__init__.py +0 -0
  4. statezero/adaptors/django/apps.py +97 -0
  5. statezero/adaptors/django/config.py +99 -0
  6. statezero/adaptors/django/context_manager.py +12 -0
  7. statezero/adaptors/django/event_emitters.py +78 -0
  8. statezero/adaptors/django/exception_handler.py +98 -0
  9. statezero/adaptors/django/extensions/__init__.py +0 -0
  10. statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
  11. statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +141 -0
  12. statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +75 -0
  13. statezero/adaptors/django/f_handler.py +312 -0
  14. statezero/adaptors/django/helpers.py +153 -0
  15. statezero/adaptors/django/middleware.py +10 -0
  16. statezero/adaptors/django/migrations/0001_initial.py +33 -0
  17. statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +16 -0
  18. statezero/adaptors/django/migrations/__init__.py +0 -0
  19. statezero/adaptors/django/orm.py +915 -0
  20. statezero/adaptors/django/permissions.py +252 -0
  21. statezero/adaptors/django/query_optimizer.py +772 -0
  22. statezero/adaptors/django/schemas.py +324 -0
  23. statezero/adaptors/django/search_providers/__init__.py +0 -0
  24. statezero/adaptors/django/search_providers/basic_search.py +24 -0
  25. statezero/adaptors/django/search_providers/postgres_search.py +51 -0
  26. statezero/adaptors/django/serializers.py +554 -0
  27. statezero/adaptors/django/urls.py +14 -0
  28. statezero/adaptors/django/views.py +336 -0
  29. statezero/core/__init__.py +34 -0
  30. statezero/core/ast_parser.py +821 -0
  31. statezero/core/ast_validator.py +266 -0
  32. statezero/core/classes.py +167 -0
  33. statezero/core/config.py +263 -0
  34. statezero/core/context_storage.py +4 -0
  35. statezero/core/event_bus.py +175 -0
  36. statezero/core/event_emitters.py +60 -0
  37. statezero/core/exceptions.py +106 -0
  38. statezero/core/interfaces.py +492 -0
  39. statezero/core/process_request.py +184 -0
  40. statezero/core/types.py +63 -0
  41. statezero-0.1.0b1.dist-info/METADATA +252 -0
  42. statezero-0.1.0b1.dist-info/RECORD +45 -0
  43. statezero-0.1.0b1.dist-info/WHEEL +5 -0
  44. statezero-0.1.0b1.dist-info/licenses/license.md +117 -0
  45. statezero-0.1.0b1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,772 @@
1
+ import logging
2
+ from django.db.models import Prefetch, QuerySet
3
+ from django.db.models.fields.related import (
4
+ ForeignObjectRel, ManyToManyField, ManyToManyRel, ForeignKey, OneToOneField, ManyToOneRel
5
+ )
6
+
7
+ from typing import Optional, Dict, Set, Callable, Type, Any, List, Union
8
+ from django.db.models import Model
9
+ from django.core.exceptions import FieldDoesNotExist, FieldError
10
+ from django.db.models.constants import LOOKUP_SEP
11
+ from contextvars import ContextVar
12
+
13
+ from statezero.core.interfaces import AbstractQueryOptimizer
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ # Cache for model metadata
18
+ _meta_cache_var = ContextVar('_meta_cache', default={})
19
+
20
+ """
21
+ This module implements a Django QuerySet optimizer that intelligently applies
22
+ `select_related`, `prefetch_related`, and optionally `.only()` to reduce the
23
+ number of database queries and the amount of data transferred.
24
+
25
+ **Vibe Coded with Gemini**
26
+
27
+ This code was co-authored with Google Gemini because the specific behaviours of the
28
+ orm are difficult to reason about. This should eventually be verified and enhanced -
29
+ the overall behaviours are verified in the tests to make sure that query counts reduce
30
+ and runtimes improve.
31
+
32
+ **Detailed Explanation:**
33
+
34
+ The core logic resides within the `optimize_query` function. This function takes a
35
+ Django QuerySet and a specification of the desired fields to retrieve as input and
36
+ returns an optimized QuerySet. It intelligently determines the optimal combination
37
+ of `select_related`, `prefetch_related`, and `.only()` calls to minimize database
38
+ interactions.
39
+
40
+ 1. **Field Path Generation and Validation (`generate_query_paths`):** The
41
+ process begins by validating the provided field paths. The
42
+ `generate_query_paths` function parses each field path (e.g.,
43
+ `'author__profile__bio'`) and verifies that each segment of the path exists
44
+ as a valid field on the corresponding model. It also identifies whether
45
+ each relationship along the path is a `ForeignKey`, `OneToOneField`,
46
+ `ManyToManyField`, or a reverse relation. This validation ensures that the
47
+ specified fields are actually accessible and helps prevent runtime errors. It
48
+ returns two structures: `all_relation_paths`, a set of all relationship paths,
49
+ and `field_map`, a dictionary mapping relation paths to the fields that should
50
+ be fetched at the end of that relationship.
51
+
52
+ 2. **Relationship Path Refinement (`refine_relationship_paths`):** After
53
+ validation, the `refine_relationship_paths` function analyzes the relationship
54
+ paths to determine whether to use `select_related` or `prefetch_related`.
55
+ `select_related` is used for `ForeignKey` and `OneToOneField` relationships,
56
+ while `prefetch_related` is used for `ManyToManyField` and reverse
57
+ relationships. The function intelligently handles nested relationships,
58
+ ensuring that the most efficient approach is used for each path.
59
+
60
+ 3. **Redundancy Removal (`remove_redundant_paths`):** This function removes
61
+ redundant paths. For example, if you request both 'a' and 'a__b', requesting
62
+ 'a' becomes redundant because 'a__b' will automatically fetch 'a' as well.
63
+
64
+ 4. **Prefetch Splitting (`_find_prefetch_split`):** This helper function finds
65
+ the first prefetch-requiring relation in a path and splits the path into a root
66
+ prefetch path and a subsequent path. This is necessary for constructing
67
+ `Prefetch` objects with inner querysets for nested optimizations.
68
+
69
+ 5. **`Prefetch` Object Construction:** For each `prefetch_related` path, the
70
+ code constructs a `Prefetch` object. It finds any nested `select_related`
71
+ paths *within* the prefetched relationship and applies them to the inner
72
+ queryset of the `Prefetch` object. It also restricts the fields fetched by the
73
+ inner queryset using `.only()` based on the specified fields_map.
74
+
75
+ 6. **`.only()` Application:** If enabled via the `use_only` parameter, the
76
+ code applies `.only()` to the root QuerySet. It includes only the fields
77
+ explicitly requested via the `fields_map`, as well as any foreign key fields
78
+ required by the `select_related` paths. This ensures that only the necessary
79
+ data is retrieved from the database.
80
+
81
+ 7. **Error Handling:** The code includes comprehensive error handling to catch
82
+ `FieldDoesNotExist`, `FieldError`, and other exceptions that may occur during
83
+ the optimization process. Error messages are logged to provide detailed
84
+ information about the cause of the error.
85
+
86
+ 8. **Caching:** Model metadata is cached to improve performance by avoiding
87
+ repeated calls to `model._meta`.
88
+
89
+ 9. **`generate_paths` Function:** This utility function is used to
90
+ automatically generate field paths based on a depth parameter and a
91
+ `fields_map`. It is used when the user does not provide an explicit list of
92
+ fields to optimize.
93
+
94
+ 10. **`DjangoQueryOptimizer` Class:** This class implements the
95
+ `AbstractQueryOptimizer` interface, providing a reusable and configurable way
96
+ to optimize Django QuerySets. It allows users to specify the depth of
97
+ relationship traversal, the fields to retrieve for each model, and a function
98
+ to get a consistent string name for a model class.
99
+
100
+ **How it Works:**
101
+
102
+ The optimizer works by analyzing the structure of the requested data and
103
+ intelligently constructing a series of `select_related`, `prefetch_related`, and
104
+ `.only()` calls. `select_related` eagerly loads related objects in the same
105
+ database query, which is efficient for `ForeignKey` and `OneToOneField`
106
+ relationships. `prefetch_related` performs a separate query for each related
107
+ object, which is necessary for `ManyToManyField` and reverse relationships.
108
+ `.only()` restricts the fields that are retrieved from the database, reducing the
109
+ amount of data transferred.
110
+
111
+ By combining these techniques, the optimizer can significantly reduce the number of
112
+ database queries and the amount of data transferred, resulting in improved
113
+ application performance.
114
+ """
115
+
116
+ def _get_model_meta(model):
117
+ """Gets cached model _meta using context variable."""
118
+ meta_cache = _meta_cache_var.get()
119
+ if model not in meta_cache:
120
+ meta_cache[model] = model._meta
121
+ # Update the context variable with the modified cache
122
+ _meta_cache_var.set(meta_cache)
123
+ return meta_cache[model]
124
+
125
+ def _clear_meta_cache():
126
+ """Clears the meta cache in the current context."""
127
+ _meta_cache_var.set({})
128
+
129
+ # ================================================================
130
+ # Path Generation & VALIDATION (Strict)
131
+ # ================================================================
132
+ def generate_query_paths(model, fields):
133
+ """Generate relationship paths and map fields, validating strictly."""
134
+ field_map = {'': set()}
135
+ all_relation_paths = set()
136
+ root_meta = _get_model_meta(model)
137
+
138
+ for field_path in fields:
139
+ parts = field_path.split(LOOKUP_SEP)
140
+ field_name = parts[-1]
141
+ relationship_parts = parts[:-1]
142
+ current_model = model
143
+ current_meta = root_meta
144
+
145
+ for i, part in enumerate(relationship_parts):
146
+ try:
147
+ field_obj = current_meta.get_field(part)
148
+ current_path_str = LOOKUP_SEP.join(relationship_parts[:i+1])
149
+ all_relation_paths.add(current_path_str)
150
+ next_model = getattr(field_obj, 'related_model', None) or \
151
+ (getattr(field_obj, 'remote_field', None) and getattr(field_obj.remote_field, 'model', None))
152
+ if not field_obj.is_relation:
153
+ raise ValueError(f"Path '{field_path}' traverses non-relational field '{part}' on {current_model.__name__}.")
154
+ if not next_model:
155
+ raise ValueError(f"Cannot determine related model for '{part}' in path '{field_path}' on {current_model.__name__}.")
156
+ current_model = next_model
157
+ current_meta = _get_model_meta(current_model)
158
+ except FieldDoesNotExist:
159
+ raise ValueError(f"Invalid path segment: '{part}' not found on {current_model.__name__} processing '{field_path}'.")
160
+ except Exception as e:
161
+ raise ValueError(f"Error processing segment '{part}' on {current_model.__name__} for path '{field_path}': {e}")
162
+
163
+ try:
164
+ current_meta.get_field(field_name)
165
+ except FieldDoesNotExist:
166
+ raise ValueError(f"Invalid final field: '{field_name}' not found on {current_model.__name__} for path '{field_path}'.")
167
+ except Exception as e:
168
+ raise ValueError(f"Error validating final field '{field_name}' on {current_model.__name__} for path '{field_path}': {e}")
169
+
170
+ relation_path_key = LOOKUP_SEP.join(relationship_parts)
171
+ field_map.setdefault(relation_path_key, set()).add(field_name)
172
+
173
+ return all_relation_paths, field_map
174
+
175
+ # ================================================================
176
+ # Refine Paths
177
+ # ================================================================
178
+ def refine_relationship_paths(model, all_relation_paths):
179
+ """Refine paths into select_related vs prefetch_related."""
180
+ select_related_paths = set()
181
+ prefetch_related_paths = set()
182
+
183
+ for path in sorted(list(all_relation_paths), key=len):
184
+ parts = path.split(LOOKUP_SEP)
185
+ current_model = model
186
+ requires_prefetch = False
187
+ valid_path = True
188
+ is_subpath_of_prefetch = any(path.startswith(p + LOOKUP_SEP) for p in prefetch_related_paths)
189
+
190
+ if is_subpath_of_prefetch:
191
+ prefetch_related_paths.add(path)
192
+ continue
193
+
194
+ for part in parts:
195
+ try:
196
+ current_meta = _get_model_meta(current_model)
197
+ field = current_meta.get_field(part)
198
+ if isinstance(field, (ManyToManyField, ManyToManyRel, ForeignObjectRel, ManyToOneRel)):
199
+ requires_prefetch = True
200
+ # Ensure related_model is valid before assignment
201
+ related_model = getattr(field, 'related_model', None)
202
+ if not related_model:
203
+ raise ValueError(f"Cannot determine related model for prefetch field '{part}' on {current_model.__name__}")
204
+ current_model = related_model
205
+ elif isinstance(field, (ForeignKey, OneToOneField)):
206
+ # Ensure remote_field and model are valid
207
+ remote_field = getattr(field, 'remote_field', None)
208
+ if not remote_field or not getattr(remote_field, 'model', None):
209
+ raise ValueError(f"Cannot determine related model for FK/O2O field '{part}' on {current_model.__name__}")
210
+ current_model = remote_field.model
211
+ else: # Should not happen with validation
212
+ logger.error(f"Unexpected non-relational field '{part}' in validated path '{path}'.")
213
+ valid_path = False; break
214
+ except Exception as e:
215
+ logger.error(f"Unexpected error refining path '{path}' at part '{part}': {e}")
216
+ valid_path = False; break
217
+
218
+ if valid_path:
219
+ if requires_prefetch:
220
+ prefetch_related_paths.add(path)
221
+ paths_to_remove = {sr for sr in select_related_paths if path.startswith(sr + LOOKUP_SEP)}
222
+ select_related_paths.difference_update(paths_to_remove)
223
+ else:
224
+ is_prefix_of_prefetch = any(pf.startswith(path + LOOKUP_SEP) for pf in prefetch_related_paths)
225
+ if not is_prefix_of_prefetch:
226
+ select_related_paths.add(path)
227
+
228
+ # Final cleanup
229
+ final_select_related = set(select_related_paths)
230
+ for sr_path in select_related_paths:
231
+ if any(pf_path.startswith(sr_path + LOOKUP_SEP) for pf_path in prefetch_related_paths):
232
+ final_select_related.discard(sr_path)
233
+
234
+ return final_select_related, prefetch_related_paths
235
+
236
+
237
+ # ================================================================
238
+ # Redundancy Removal
239
+ # ================================================================
240
+ def remove_redundant_paths(paths):
241
+ """Remove redundant paths (e.g., 'a' if 'a__b' exists)."""
242
+ if not paths: return set()
243
+ # Sort by length descending to check longer paths against shorter ones
244
+ sorted_paths = sorted(list(paths), key=len, reverse=True)
245
+ result = set(sorted_paths) # Start with all paths
246
+ to_remove = set() # Keep track of paths to remove
247
+
248
+ for i, long_path in enumerate(sorted_paths):
249
+ # If long_path itself was already removed, skip checks for it
250
+ if long_path in to_remove:
251
+ continue
252
+ # Check against all shorter paths that come after it in the sorted list
253
+ for j in range(i + 1, len(sorted_paths)):
254
+ short_path = sorted_paths[j]
255
+ # If short_path was already marked for removal, skip
256
+ if short_path in to_remove:
257
+ continue
258
+ # Check if the long path starts with the short path + separator
259
+ if long_path.startswith(short_path + LOOKUP_SEP):
260
+ # Mark the shorter path for removal
261
+ logger.debug(f"Marking '{short_path}' for removal because '{long_path}' exists.")
262
+ to_remove.add(short_path)
263
+
264
+ # Remove the marked paths from the result set
265
+ result.difference_update(to_remove)
266
+ logger.debug(f"remove_redundant_paths input: {paths}")
267
+ logger.debug(f"remove_redundant_paths output: {result}")
268
+ return result
269
+
270
+ # ================================================================
271
+ # Prefetch Split Helper
272
+ # ================================================================
273
+ def _find_prefetch_split(start_model, path):
274
+ """Finds the first prefetch-requiring relation and splits the path."""
275
+ current_model = start_model
276
+ parts = path.split(LOOKUP_SEP)
277
+ root_prefetch_list = []
278
+ subsequent_list = []
279
+ related_model_after_root = None
280
+ prefetch_found = False
281
+
282
+ for i, part in enumerate(parts):
283
+ try:
284
+ current_meta = _get_model_meta(current_model)
285
+ field = current_meta.get_field(part)
286
+ is_prefetch_relation = isinstance(field, (ManyToManyField, ManyToManyRel, ForeignObjectRel, ManyToOneRel))
287
+ next_model = getattr(field, 'related_model', None) or \
288
+ (getattr(field, 'remote_field', None) and getattr(field.remote_field, 'model', None))
289
+
290
+ if not prefetch_found:
291
+ root_prefetch_list.append(part)
292
+ # Need the next model to continue, even if prefetch not found yet
293
+ if not next_model and i < len(parts) - 1: # Check if not the last part
294
+ raise ValueError(f"Cannot determine next model for non-prefetch part '{part}' in '{path}'")
295
+ current_model = next_model
296
+ if is_prefetch_relation:
297
+ prefetch_found = True
298
+ related_model_after_root = current_model # The model *being* prefetched
299
+ else:
300
+ subsequent_list.append(part)
301
+ # Need to continue stepping through models for subsequent path
302
+ if not next_model and i < len(parts) - 1:
303
+ raise ValueError(f"Cannot determine next model for subsequent part '{part}' in '{path}'")
304
+ current_model = next_model
305
+
306
+ except Exception as e:
307
+ logger.error(f"Error splitting path '{path}' at part '{part}': {e}")
308
+ return None, None, None # Return three Nones
309
+
310
+ if prefetch_found:
311
+ root_prefetch_path = LOOKUP_SEP.join(root_prefetch_list)
312
+ subsequent_path = LOOKUP_SEP.join(subsequent_list)
313
+ return root_prefetch_path, subsequent_path, related_model_after_root
314
+ else:
315
+ # This path didn't actually contain a prefetch relation
316
+ logger.warning(f"Path '{path}' ended up in prefetch logic but contained no prefetch relation.")
317
+ return None, None, None
318
+
319
+ # ================================================================
320
+ # MAIN OPTIMIZATION FUNCTION
321
+ # ================================================================
322
+ def optimize_query(queryset, fields=None, fields_map=None, depth=0, use_only=True, get_model_name=None):
323
+ """
324
+ Apply select_related, prefetch_related, and optionally .only() optimizations.
325
+ Uses either:
326
+ 1. A list of field paths (fields). In this case it still relies on the field map to get which models will be selected.
327
+ 2. A fields_map and depth to automatically generate paths.
328
+
329
+ Args:
330
+ queryset: Django QuerySet
331
+ fields (list, optional): List of field paths.
332
+ fields_map (dict, optional): Dictionary specifying fields to retrieve for each model,
333
+ with model names obtained using get_model_name.
334
+ depth (int, optional): Depth of relationships to traverse when using fields_map.
335
+ use_only (bool): If True, use .only() on the root model.
336
+ get_model_name (callable, optional): Function to get model name from a model class.
337
+ Required if using fields_map or if 'fields' is used with 'fields_map'.
338
+
339
+ Returns:
340
+ QuerySet: Optimized queryset
341
+ """
342
+ if not isinstance(queryset, QuerySet):
343
+ raise TypeError("queryset must be a Django QuerySet instance.")
344
+
345
+ model = queryset.model
346
+ _clear_meta_cache()
347
+
348
+ # Validate get_model_name if fields_map is used or fields is used along with fields_map
349
+ if (fields_map or fields) and not callable(get_model_name):
350
+ raise ValueError("If 'fields_map' or 'fields' with 'fields_map' is provided, 'get_model_name' must be a callable function.")
351
+
352
+ # 1. Generate paths either from explicit field list or fields_map/depth
353
+ if fields:
354
+ try:
355
+ all_relation_paths, field_map = generate_query_paths(model, fields)
356
+ except ValueError as e:
357
+ logger.error(f"Input field validation failed: {e}")
358
+ _clear_meta_cache()
359
+ raise
360
+
361
+ elif fields_map:
362
+ # Generate paths from fields_map and depth
363
+ if get_model_name is None:
364
+ raise ValueError("get_model_name must be provided when using fields_map")
365
+ generated_paths = generate_paths(model, depth, fields_map, get_model_name)
366
+ fields = list(generated_paths) # Convert set to list
367
+ #Generate fields from generated paths to be used in only clause for the root model
368
+
369
+ all_relation_paths = set()
370
+ field_map = {'': set()}
371
+
372
+ for field_path in fields:
373
+ parts = field_path.split(LOOKUP_SEP)
374
+ field_name = parts[-1]
375
+ relationship_parts = parts[:-1]
376
+ relation_path_key = LOOKUP_SEP.join(relationship_parts)
377
+ field_map.setdefault(relation_path_key, set()).add(field_name)
378
+ if relationship_parts:
379
+ all_relation_paths.add(relation_path_key)
380
+
381
+
382
+ else:
383
+ logger.info("No fields or fields_map specified, returning original queryset.")
384
+ return queryset
385
+
386
+ # --- Continue with optimization ---
387
+ try:
388
+ # 2. Determine which paths use select_related vs prefetch_related
389
+ select_related_paths, prefetch_related_paths = refine_relationship_paths(
390
+ model, all_relation_paths
391
+ )
392
+
393
+ # 3. Remove redundant paths for top-level application
394
+ final_select_related = remove_redundant_paths(select_related_paths)
395
+
396
+ logger.debug(f"--- Optimization Plan for {model.__name__} ---")
397
+ logger.debug(f" Input Fields (Validated): {fields}")
398
+ logger.debug(f" Final Select Related: {final_select_related}")
399
+ logger.debug(f" All Prefetch Paths (to process): {prefetch_related_paths}")
400
+ logger.debug(f" Field Map (Validated): {field_map}")
401
+ logger.debug(f" Use Only (Root): {use_only}") # Log use_only parameter
402
+ logger.debug(f" Fields Map (Passed In): {fields_map}")
403
+
404
+ prefetch_data = {} # Dictionary to store Prefetch build info
405
+
406
+ # Apply top-level select_related first
407
+ if final_select_related:
408
+ logger.info(f"Applying select_related({final_select_related})")
409
+ queryset = queryset.select_related(*final_select_related)
410
+ else:
411
+ logger.info("No select_related paths to apply.")
412
+
413
+ # ================================================================
414
+ # Build Prefetch objects
415
+ # ================================================================
416
+ processed_prefetch_roots = set() # Track roots to build only one Prefetch per root path
417
+ prefetch_objects = []
418
+
419
+ # Process prefetch paths, potentially building nested select_related inside
420
+ for path in prefetch_related_paths:
421
+ split_result = _find_prefetch_split(model, path)
422
+ if not split_result or not split_result[0]:
423
+ logger.debug(f"Skipping prefetch build for path '{path}' - split failed or no prefetch found.")
424
+ continue # Skip if path doesn't represent a valid prefetch structure
425
+
426
+ root_pf_path, subsequent_path, related_model = split_result
427
+
428
+ if not related_model:
429
+ logger.warning(f"Cannot determine related model for prefetch '{root_pf_path}'. Skipping.")
430
+ continue
431
+
432
+ # Aggregate nested select info for this root path
433
+ if root_pf_path not in prefetch_data:
434
+ prefetch_data[root_pf_path] = {
435
+ 'related_model': related_model,
436
+ 'nested_selects': set(),
437
+ }
438
+ # Add subsequent path if it represents a valid nested select_related chain
439
+ if subsequent_path:
440
+ # Basic check: Does subsequent path contain prefetch-like relations? If not, assume select_related.
441
+ # (More robust check could re-run refine_paths logic on subsequent path relative to related_model)
442
+ is_nested_select = True
443
+ current_nested_model = related_model
444
+ try:
445
+ for part in subsequent_path.split(LOOKUP_SEP):
446
+ meta = _get_model_meta(current_nested_model)
447
+ field = meta.get_field(part)
448
+ if not isinstance(field, (ForeignKey, OneToOneField)):
449
+ is_nested_select = False; break
450
+ current_nested_model = field.remote_field.model
451
+ except Exception:
452
+ is_nested_select = False
453
+
454
+ if is_nested_select:
455
+ prefetch_data[root_pf_path]['nested_selects'].add(subsequent_path)
456
+ else:
457
+ logger.debug(f"Subsequent path '{subsequent_path}' for root '{root_pf_path}' is not purely select_related.")
458
+
459
+ # --- Now, build the actual Prefetch objects ---
460
+ for root_pf_path, pf_info in prefetch_data.items():
461
+ # Avoid creating duplicate Prefetch objects for the same root
462
+ if root_pf_path in processed_prefetch_roots:
463
+ continue
464
+
465
+ related_model = pf_info['related_model']
466
+ inner_queryset = related_model._default_manager.all()
467
+
468
+ # Apply nested select_related if any were found for this root
469
+ final_nested_selects = remove_redundant_paths(pf_info['nested_selects'])
470
+ if final_nested_selects:
471
+ logger.debug(f" Applying nested select_related({final_nested_selects}) within Prefetch('{root_pf_path}')")
472
+ inner_queryset = inner_queryset.select_related(*final_nested_selects)
473
+
474
+ # --- Apply .only() to the INNER queryset (the one *being* prefetched) ---
475
+ related_model_name = get_model_name(related_model)
476
+
477
+ related_fields_to_fetch = set()
478
+
479
+ if fields_map and related_model_name in fields_map:
480
+ related_fields_to_fetch.update(fields_map[related_model_name])
481
+ else:
482
+ # If no field restrictions are provided, get all fields
483
+ all_fields = [f.name for f in related_model._meta.get_fields() if f.concrete]
484
+ related_fields_to_fetch.update(all_fields)
485
+ logger.debug(f"No fields_map provided for {related_model_name}. Fetching all fields.")
486
+
487
+ # Always add PK
488
+ related_fields_to_fetch.add(related_model._meta.pk.name)
489
+
490
+ if related_fields_to_fetch:
491
+ logger.debug(f" Applying .only({related_fields_to_fetch}) to inner queryset for Prefetch('{root_pf_path}')")
492
+ try:
493
+ inner_queryset = inner_queryset.only(*related_fields_to_fetch)
494
+ except FieldError as e:
495
+ logger.error(f"FieldError applying .only({related_fields_to_fetch}) to {related_model_name} for prefetch: {e}")
496
+ raise
497
+
498
+ # Create the final Prefetch object
499
+ prefetch_obj = Prefetch(root_pf_path, queryset=inner_queryset)
500
+ prefetch_objects.append(prefetch_obj)
501
+ processed_prefetch_roots.add(root_pf_path)
502
+
503
+ # Construct representation for logging
504
+ qs_repr_parts = [f"{related_model.__name__}.objects"]
505
+ if final_nested_selects:
506
+ qs_repr_parts.append(f".select_related({final_nested_selects})")
507
+ if related_fields_to_fetch:
508
+ qs_repr_parts.append(f".only({related_fields_to_fetch})")
509
+ qs_repr = "".join(qs_repr_parts)
510
+ logger.info(f"Prepared Prefetch('{root_pf_path}', queryset={qs_repr})")
511
+
512
+ # Apply prefetch_related with the constructed objects
513
+ if prefetch_objects:
514
+ logger.info(f"Applying prefetch_related with {len(prefetch_objects)} optimized Prefetch objects.")
515
+ queryset = queryset.prefetch_related(*prefetch_objects) # Apply unique prefetches
516
+ else:
517
+ logger.info("No prefetch_related paths requiring optimized Prefetch objects.")
518
+
519
+ # --- Apply .only() for the ROOT queryset IF use_only is True ---
520
+ # This section is restored to its state before use_only was removed
521
+ apply_only = False
522
+ root_fields_to_fetch = set()
523
+
524
+ if use_only: # Check the parameter
525
+ root_meta = _get_model_meta(model)
526
+ pk_name = root_meta.pk.name
527
+
528
+ # Add direct non-relational fields requested for the root model
529
+ if '' in field_map:
530
+ for field_name in field_map.get('', set()):
531
+ try:
532
+ field_obj = root_meta.get_field(field_name)
533
+ if not field_obj.is_relation:
534
+ root_fields_to_fetch.add(field_name)
535
+ elif isinstance(field_obj, (ForeignKey, OneToOneField)):
536
+ # If FK/O2O itself is requested directly, include its id field
537
+ root_fields_to_fetch.add(field_obj.attname)
538
+ except FieldDoesNotExist: # Should not happen after validation
539
+ logger.error(f"Validated field '{field_name}' unexpectedly not found on root model {model.__name__} during .only() phase.")
540
+ except Exception as e:
541
+ logger.error(f"Error processing root field '{field_name}' for .only(): {e}")
542
+
543
+ # Always include the primary key if using .only()
544
+ if pk_name: root_fields_to_fetch.add(pk_name)
545
+
546
+ # Add the foreign key fields (_id) required by top-level select_related paths
547
+ if final_select_related:
548
+ for path in final_select_related:
549
+ first_part = path.split(LOOKUP_SEP)[0]
550
+ try:
551
+ field_obj = root_meta.get_field(first_part)
552
+ # Only add FK/O2O attribute names (e.g., 'author_id')
553
+ if isinstance(field_obj, (ForeignKey, OneToOneField)):
554
+ root_fields_to_fetch.add(field_obj.attname)
555
+ except FieldDoesNotExist: # Should not happen
556
+ logger.error(f"Validated field '{first_part}' from select_related path '{path}' unexpectedly not found on {model.__name__} during .only() phase.")
557
+ except Exception as e:
558
+ logger.error(f"Error processing select_related path '{path}' for .only(): {e}")
559
+
560
+ # Determine if .only() should actually be applied
561
+ if root_fields_to_fetch:
562
+ apply_only = True # Set flag to true only if use_only=True and fields were found
563
+ else:
564
+ apply_only = False
565
+ logger.warning(f"use_only=True but no root fields identified for .only() on {model.__name__}. Not applying .only().")
566
+
567
+ # Apply .only() based on the apply_only flag (which depends on use_only)
568
+ if apply_only:
569
+ logger.info(f"Applying .only({root_fields_to_fetch}) to root queryset.")
570
+ try:
571
+ queryset = queryset.only(*root_fields_to_fetch)
572
+ except FieldError as e:
573
+ logger.error(f"FieldError applying .only({root_fields_to_fetch}) to {model.__name__}: {e}. Check for conflicts with annotations or ordering.")
574
+ raise # Re-raise FieldError as it indicates a real problem
575
+ # No 'elif apply_defer' block anymore
576
+ else:
577
+ # This logs if use_only=False OR if use_only=True but no fields were calculated
578
+ logger.info("Not applying .only() to root queryset (use_only=False or no fields identified).")
579
+
580
+ # --- Error Handling ---
581
+ except FieldError as e:
582
+ # Catch FieldErrors that might occur during select_related/prefetch_related too
583
+ logger.error(f"FieldError during optimization application: {e}.")
584
+ _clear_meta_cache()
585
+ raise e
586
+ except ValueError as e: # Catch validation errors
587
+ logger.error(f"Field validation or processing error: {e}")
588
+ _clear_meta_cache()
589
+ raise e
590
+ except Exception as e:
591
+ logger.exception(f"An unexpected error occurred during query optimization: {e}")
592
+ _clear_meta_cache()
593
+ raise e
594
+
595
+ _clear_meta_cache()
596
+ logger.debug(f"--- Optimization finished for {model.__name__} ---")
597
+ return queryset
598
+
599
+ # ================================================================
600
+ # generate_paths Helper (No changes needed from original provided)
601
+ # ================================================================
602
+ def generate_paths(model, depth, fields, get_model_name):
603
+ """
604
+ Generates relationship paths up to a given depth for specified fields dict.
605
+ """
606
+ paths = set()
607
+ processed_models = set() # Avoid infinite loops
608
+
609
+ def _traverse(current_model, current_path, current_depth):
610
+ model_identifier = (current_model, current_path)
611
+ if current_depth > depth or model_identifier in processed_models:
612
+ return
613
+ processed_models.add(model_identifier)
614
+
615
+ model_name = get_model_name(current_model)
616
+ current_meta = _get_model_meta(current_model)
617
+
618
+ if model_name in fields:
619
+ model_fields_to_include = fields[model_name]
620
+ for field_name in model_fields_to_include:
621
+ try:
622
+ field_obj = current_meta.get_field(field_name)
623
+ full_path = current_path + (LOOKUP_SEP if current_path else "") + field_name
624
+ paths.add(full_path) # Add the path ending here
625
+
626
+ # If it's a relation and we should traverse further
627
+ if field_obj.is_relation:
628
+ related_model = getattr(field_obj, 'related_model', None) or \
629
+ (getattr(field_obj, 'remote_field', None) and getattr(field_obj.remote_field, 'model', None))
630
+ if related_model and get_model_name(related_model) in fields:
631
+ _traverse(related_model, full_path, current_depth + 1)
632
+ except FieldDoesNotExist:
633
+ logger.warning(f"[generate_paths] Field '{field_name}' specified in 'fields' dict not found on model {model_name} at path '{current_path}'. Skipping.")
634
+ continue
635
+
636
+ _traverse(model, "", 0)
637
+ _clear_meta_cache()
638
+ logger.debug(f"[generate_paths] Generated paths: {paths}")
639
+ # Note: This generate_paths does basic path building based on the dict keys/values.
640
+ # It does *not* guarantee the same level of strict validation as the internal generate_query_paths.
641
+ # The main optimize_query function relies on its *internal* generate_query_paths for validation.
642
+ return paths
643
+
644
+ def optimize_individual_model(model_instance, fields_map=None, depth=0, use_only=True, get_model_name=None):
645
+ """
646
+ Optimizes fetching a single model instance using select_related, prefetch_related, and .only().
647
+ """
648
+ if not isinstance(model_instance, Model):
649
+ raise TypeError("model_instance must be a Django Model instance.")
650
+
651
+ model_class = model_instance.__class__
652
+
653
+ #Check for related fields before proceeding to optimization
654
+ any_related_fields = False
655
+
656
+ if fields_map:
657
+ for model_name, model_fields in fields_map.items():
658
+ for field in model_fields:
659
+ if '__' in field: # If there's a related field its length is >1
660
+ any_related_fields = True
661
+ break
662
+ if any_related_fields:
663
+ break
664
+ #If there are no related fields, return the instance with no extra queries.
665
+ if not any_related_fields:
666
+ logger.info("No related fields requested. Skipping optimization.")
667
+ return model_instance
668
+ try:
669
+ # 1. Turn the instance into a queryset.
670
+ queryset = model_class.objects.filter(pk=model_instance.pk)
671
+
672
+ # 2. Optimize the queryset using the shared optimization logic.
673
+ optimized_queryset = optimize_query(
674
+ queryset,
675
+ fields=None, #Let fields_map handle field validation and path creation
676
+ fields_map=fields_map,
677
+ depth=depth,
678
+ use_only=use_only,
679
+ get_model_name=get_model_name
680
+ )
681
+
682
+ # 3. Extract the optimized instance.
683
+ optimized_instance = optimized_queryset.first()
684
+
685
+ return optimized_instance
686
+
687
+ except Exception as e:
688
+ logger.exception(f"An error occurred during individual model optimization: {e}")
689
+ raise
690
+
691
+ class DjangoQueryOptimizer(AbstractQueryOptimizer):
692
+ """
693
+ Concrete implementation of AbstractQueryOptimizer for Django QuerySets.
694
+ """
695
+ def __init__(
696
+ self,
697
+ depth: Optional[int] = None,
698
+ fields_per_model: Optional[Dict[str, Set[str]]] = None,
699
+ get_model_name_func: Optional[Callable[[Type[Model]], str]] = None,
700
+ use_only: bool = True
701
+ ):
702
+ """
703
+ Initializes the optimizer with configuration parameters.
704
+
705
+ Args:
706
+ depth (Optional[int]): Maximum relationship traversal depth
707
+ if generating field paths automatically.
708
+ fields_per_model (Optional[Dict[str, Set[str]]]): Mapping of
709
+ model names (keys) to sets of required field/relationship names
710
+ (values), used if generating field paths automatically.
711
+ get_model_name_func (Optional[Callable]): Function to get a
712
+ consistent string name for a model class.
713
+ use_only (bool): Whether to use .only() on the root model.
714
+ """
715
+ self.depth = depth
716
+ self.fields_per_model = fields_per_model
717
+ self.get_model_name_func = get_model_name_func
718
+ self.use_only = use_only
719
+
720
+ # Validate configuration
721
+ if (fields_per_model or depth is not None) and not get_model_name_func:
722
+ raise ValueError("If 'fields_per_model' or 'depth' is provided, 'get_model_name_func' must also be provided.")
723
+
724
+ if depth is not None and depth < 0:
725
+ raise ValueError("Depth cannot be negative.")
726
+
727
+ def optimize(
728
+ self,
729
+ queryset: Union[QuerySet, Model],
730
+ fields: Optional[List[str]] = None
731
+ ) -> Union[QuerySet, Model]:
732
+ """
733
+ Optimizes the given Django QuerySet or Model instance.
734
+
735
+ Args:
736
+ queryset (Union[QuerySet, Model]): The Django QuerySet or Model instance to optimize.
737
+ fields (Optional[List[str]]): An explicit list of field paths to optimize for.
738
+ If provided, this overrides automatic path generation.
739
+ **kwargs: Optional overrides for depth, fields_map, get_model_name_func, or use_only.
740
+
741
+ Returns:
742
+ Union[QuerySet, Model]: The optimized QuerySet or Model instance.
743
+ """
744
+ # Handle optional overrides
745
+ depth = self.depth
746
+ fields_map = self.fields_per_model
747
+ get_model_name_func = self.get_model_name_func
748
+ use_only = self.use_only
749
+
750
+ if isinstance(queryset, Model):
751
+ # Optimize a single model instance
752
+ return optimize_individual_model(
753
+ queryset,
754
+ fields_map=fields_map,
755
+ depth=depth,
756
+ use_only=use_only,
757
+ get_model_name=get_model_name_func
758
+ )
759
+ elif isinstance(queryset, QuerySet):
760
+ #Optimize a queryset object.
761
+ optimized_queryset = optimize_query(
762
+ queryset,
763
+ fields=fields,
764
+ fields_map=fields_map,
765
+ depth=depth,
766
+ use_only=use_only,
767
+ get_model_name=get_model_name_func,
768
+ )
769
+
770
+ return optimized_queryset
771
+ else:
772
+ raise TypeError("Input must be a QuerySet or a Model instance.")