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.
- statezero/__init__.py +0 -0
- statezero/adaptors/__init__.py +0 -0
- statezero/adaptors/django/__init__.py +0 -0
- statezero/adaptors/django/apps.py +97 -0
- statezero/adaptors/django/config.py +99 -0
- statezero/adaptors/django/context_manager.py +12 -0
- statezero/adaptors/django/event_emitters.py +78 -0
- statezero/adaptors/django/exception_handler.py +98 -0
- statezero/adaptors/django/extensions/__init__.py +0 -0
- statezero/adaptors/django/extensions/custom_field_serializers/__init__.py +0 -0
- statezero/adaptors/django/extensions/custom_field_serializers/file_fields.py +141 -0
- statezero/adaptors/django/extensions/custom_field_serializers/money_field.py +75 -0
- statezero/adaptors/django/f_handler.py +312 -0
- statezero/adaptors/django/helpers.py +153 -0
- statezero/adaptors/django/middleware.py +10 -0
- statezero/adaptors/django/migrations/0001_initial.py +33 -0
- statezero/adaptors/django/migrations/0002_delete_modelviewsubscription.py +16 -0
- statezero/adaptors/django/migrations/__init__.py +0 -0
- statezero/adaptors/django/orm.py +915 -0
- statezero/adaptors/django/permissions.py +252 -0
- statezero/adaptors/django/query_optimizer.py +772 -0
- statezero/adaptors/django/schemas.py +324 -0
- statezero/adaptors/django/search_providers/__init__.py +0 -0
- statezero/adaptors/django/search_providers/basic_search.py +24 -0
- statezero/adaptors/django/search_providers/postgres_search.py +51 -0
- statezero/adaptors/django/serializers.py +554 -0
- statezero/adaptors/django/urls.py +14 -0
- statezero/adaptors/django/views.py +336 -0
- statezero/core/__init__.py +34 -0
- statezero/core/ast_parser.py +821 -0
- statezero/core/ast_validator.py +266 -0
- statezero/core/classes.py +167 -0
- statezero/core/config.py +263 -0
- statezero/core/context_storage.py +4 -0
- statezero/core/event_bus.py +175 -0
- statezero/core/event_emitters.py +60 -0
- statezero/core/exceptions.py +106 -0
- statezero/core/interfaces.py +492 -0
- statezero/core/process_request.py +184 -0
- statezero/core/types.py +63 -0
- statezero-0.1.0b1.dist-info/METADATA +252 -0
- statezero-0.1.0b1.dist-info/RECORD +45 -0
- statezero-0.1.0b1.dist-info/WHEEL +5 -0
- statezero-0.1.0b1.dist-info/licenses/license.md +117 -0
- 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.")
|