Skip to content

Commit bcc1f57

Browse files
committed
add support for QuerySet.explain()
1 parent bdd2a40 commit bcc1f57

File tree

4 files changed

+55
-3
lines changed

4 files changed

+55
-3
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,15 @@ 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 `verbosity` option](https://www.mongodb.com/docs/manual/reference/method/cursor.explain/#behavior).
118+
119+
Example: `QuerySet.explain(verbosity="...")`
120+
121+
Valid values are "queryPlanner" (default), "executionStats", and
122+
"allPlansExecution".
123+
115124
## Known issues and limitations
116125

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

django_mongodb/compiler.py

Lines changed: 31 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
@@ -490,6 +491,36 @@ def _get_ordering(self):
490491
def get_where(self):
491492
return self.where
492493

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

494525
class SQLInsertCompiler(SQLCompiler):
495526
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 = {"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)