Skip to content

add support for QuerySet.explain() #106

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ And whenever you run `python manage.py startapp`, you must remove the line:

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

## Notes on Django QuerySets

* `QuerySet.explain()` supports the [`comment` and `verbosity` options](
https://www.mongodb.com/docs/manual/reference/command/explain/#command-fields).

Example: `QuerySet.explain(comment="...", verbosity="...")`

Valid values for `verbosity` are `"queryPlanner"` (default),
`"executionStats"`, and `"allPlansExecution"`.

## Known issues and limitations

- The following `QuerySet` methods aren't supported:
Expand Down
32 changes: 32 additions & 0 deletions django_mongodb/compiler.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import itertools
import pprint
from collections import defaultdict

from bson import SON
Expand Down Expand Up @@ -491,6 +492,37 @@ def _get_ordering(self):
def get_where(self):
return self.where

def explain_query(self):
# Validate format (none supported) and options.
options = self.connection.ops.explain_query_prefix(
self.query.explain_info.format,
**self.query.explain_info.options,
)
# Build the query pipeline.
self.pre_sql_setup()
columns = self.get_columns()
query = self.build_query(
# Avoid $project (columns=None) if unneeded.
columns if self.query.annotations or not self.query.default_cols else None
)
pipeline = query.get_pipeline()
# Explain the pipeline.
kwargs = {}
for option in self.connection.ops.explain_options:
if value := options.get(option):
kwargs[option] = value
explain = self.connection.database.command(
"explain",
{"aggregate": self.collection_name, "pipeline": pipeline, "cursor": {}},
**kwargs,
)
# Generate the output: a list of lines that Django joins with newlines.
result = []
for key, value in explain.items():
formatted_value = pprint.pformat(value, indent=4)
result.append(f"{key}: {formatted_value}")
return result


class SQLInsertCompiler(SQLCompiler):
def execute_sql(self, returning_fields=None):
Expand Down
4 changes: 1 addition & 3 deletions django_mongodb/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
has_json_object_function = False
has_native_json_field = True
supports_date_lookup_using_string = False
supports_explaining_query_execution = True
supports_foreign_keys = False
supports_ignore_conflicts = False
supports_json_field_contains = False
Expand Down Expand Up @@ -53,9 +54,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
"expressions.tests.IterableLookupInnerExpressionsTests.test_expressions_in_lookups_join_choice",
# Unexpected alias_refcount in alias_map.
"queries.tests.Queries1Tests.test_order_by_tables",
# QuerySet.explain() not implemented:
# https://github.com/mongodb-labs/django-mongodb/issues/28
"queries.test_explain.ExplainUnsupportedTests.test_message",
# The $sum aggregation returns 0 instead of None for null.
"aggregation.test_filter_argument.FilteredAggregateTests.test_plain_annotate",
"aggregation.tests.AggregateTestCase.test_aggregation_default_passed_another_aggregate",
Expand Down
14 changes: 14 additions & 0 deletions django_mongodb/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class DatabaseOperations(BaseDatabaseOperations):
Combinable.BITOR: "bitOr",
Combinable.BITXOR: "bitXor",
}
explain_options = {"comment", "verbosity"}

def adapt_datefield_value(self, value):
"""Store DateField as datetime."""
Expand Down Expand Up @@ -198,6 +199,19 @@ def _prep_lookup_value(self, value, field, field_kind, lookup):
value = self.adapt_decimalfield_value(value, field.max_digits, field.decimal_places)
return value

def explain_query_prefix(self, format=None, **options):
# Validate options.
validated_options = {}
if options:
for valid_option in self.explain_options:
value = options.pop(valid_option, None)
if value is not None:
validated_options[valid_option] = value
# super() raises an error if any options are left after the valid ones
# are popped above.
super().explain_query_prefix(format, **options)
return validated_options

"""Django uses these methods to generate SQL queries before it generates MQL queries."""

# EXTRACT format cannot be passed in parameters.
Expand Down