Skip to content

Commit 143079b

Browse files
committed
add support for QuerySet.explain()
1 parent c8869b8 commit 143079b

File tree

4 files changed

+57
-3
lines changed

4 files changed

+57
-3
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,16 @@ And whenever you run `python manage.py startapp`, you must remove the line:
112112

113113
from the new application's `apps.py` file.
114114

115+
## Notes on Django QuerySets
116+
117+
* `QuerySet.explain()` supports the [`comment` and `verbosity` options](
118+
https://www.mongodb.com/docs/manual/reference/command/explain/#command-fields).
119+
120+
Example: `QuerySet.explain(comment="...", verbosity="...")`
121+
122+
Valid values for `verbosity` are `"queryPlanner"` (default),
123+
`"executionStats"`, and `"allPlansExecution"`.
124+
115125
## Known issues and limitations
116126

117127
- The following `QuerySet` methods aren't supported:

django_mongodb/compiler.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import itertools
2+
import pprint
23
from collections import defaultdict
34

45
from bson import SON
@@ -491,6 +492,37 @@ def _get_ordering(self):
491492
def get_where(self):
492493
return self.where
493494

495+
def explain_query(self):
496+
# Validate format (none supported) and options.
497+
options = self.connection.ops.explain_query_prefix(
498+
self.query.explain_info.format,
499+
**self.query.explain_info.options,
500+
)
501+
# Build the query pipeline.
502+
self.pre_sql_setup()
503+
columns = self.get_columns()
504+
query = self.build_query(
505+
# Avoid $project (columns=None) if unneeded.
506+
columns if self.query.annotations or not self.query.default_cols else None
507+
)
508+
pipeline = query.get_pipeline()
509+
# Explain the pipeline.
510+
kwargs = {}
511+
for option in self.connection.ops.explain_options:
512+
if value := options.get(option):
513+
kwargs[option] = value
514+
explain = self.connection.database.command(
515+
"explain",
516+
{"aggregate": self.collection_name, "pipeline": pipeline, "cursor": {}},
517+
**kwargs,
518+
)
519+
# Generate the output: a list of lines that Django joins with newlines.
520+
result = []
521+
for key, value in explain.items():
522+
formatted_value = pprint.pformat(value, indent=4)
523+
result.append(f"{key}: {formatted_value}")
524+
return result
525+
494526

495527
class SQLInsertCompiler(SQLCompiler):
496528
def execute_sql(self, returning_fields=None):

django_mongodb/features.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
88
has_json_object_function = False
99
has_native_json_field = True
1010
supports_date_lookup_using_string = False
11+
supports_explaining_query_execution = True
1112
supports_foreign_keys = False
1213
supports_ignore_conflicts = False
1314
supports_json_field_contains = False
@@ -53,9 +54,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
5354
"expressions.tests.IterableLookupInnerExpressionsTests.test_expressions_in_lookups_join_choice",
5455
# Unexpected alias_refcount in alias_map.
5556
"queries.tests.Queries1Tests.test_order_by_tables",
56-
# QuerySet.explain() not implemented:
57-
# https://github.com/mongodb-labs/django-mongodb/issues/28
58-
"queries.test_explain.ExplainUnsupportedTests.test_message",
5957
# The $sum aggregation returns 0 instead of None for null.
6058
"aggregation.test_filter_argument.FilteredAggregateTests.test_plain_annotate",
6159
"aggregation.tests.AggregateTestCase.test_aggregation_default_passed_another_aggregate",

django_mongodb/operations.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class DatabaseOperations(BaseDatabaseOperations):
2626
Combinable.BITOR: "bitOr",
2727
Combinable.BITXOR: "bitXor",
2828
}
29+
explain_options = {"comment", "verbosity"}
2930

3031
def adapt_datefield_value(self, value):
3132
"""Store DateField as datetime."""
@@ -198,6 +199,19 @@ def _prep_lookup_value(self, value, field, field_kind, lookup):
198199
value = self.adapt_decimalfield_value(value, field.max_digits, field.decimal_places)
199200
return value
200201

202+
def explain_query_prefix(self, format=None, **options):
203+
# Validate options.
204+
validated_options = {}
205+
if options:
206+
for valid_option in self.explain_options:
207+
value = options.pop(valid_option, None)
208+
if value is not None:
209+
validated_options[valid_option] = value
210+
# super() raises an error if any options are left after the valid ones
211+
# are popped above.
212+
super().explain_query_prefix(format, **options)
213+
return validated_options
214+
201215
"""Django uses these methods to generate SQL queries before it generates MQL queries."""
202216

203217
# EXTRACT format cannot be passed in parameters.

0 commit comments

Comments
 (0)