Skip to content

Make QuerySet.explain() return parsable JSON #340

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
Jul 21, 2025
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: 2 additions & 8 deletions django_mongodb_backend/compiler.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import itertools
import pprint
from collections import defaultdict

from bson import SON
from bson import SON, json_util
from django.core.exceptions import EmptyResultSet, FieldError, FullResultSet
from django.db import IntegrityError, NotSupportedError
from django.db.models import Count
Expand Down Expand Up @@ -652,12 +651,7 @@ def explain_query(self):
{"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
return [json_util.dumps(explain, indent=4, ensure_ascii=False)]


class SQLInsertCompiler(SQLCompiler):
Expand Down
20 changes: 20 additions & 0 deletions docs/source/ref/models/querysets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ In addition, :meth:`QuerySet.delete() <django.db.models.query.QuerySet.delete>`
and :meth:`update() <django.db.models.query.QuerySet.update>` do not support
queries that span multiple collections.

.. _queryset-explain:

``QuerySet.explain()``
======================

Expand All @@ -29,6 +31,24 @@ Example::
Valid values for ``verbosity`` are ``"queryPlanner"`` (default),
``"executionStats"``, and ``"allPlansExecution"``.

The result of ``explain()`` is a string::

>>> print(Model.objects.explain())
{
"explainVersion": "1",
"queryPlanner": {
...
},
...
}

that can be parsed as JSON::

>>> from bson import json_util
>>> result = Model.objects.filter(name="MongoDB").explain()
>>> json_util.loads(result)['command']["pipeline"]
[{'$match': {'$expr': {'$eq': ['$name', 'MongoDB']}}}]

MongoDB-specific ``QuerySet`` methods
=====================================

Expand Down
2 changes: 2 additions & 0 deletions docs/source/releases/5.2.x.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Bug fixes

- Fixed ``RecursionError`` when using ``Trunc`` database functions on non-MongoDB
databases.
- :meth:`QuerySet.explain() <django.db.models.query.QuerySet.explain>` now
:ref:`returns a string that can be parsed as JSON <queryset-explain>`.

5.2.0 beta 1
============
Expand Down
37 changes: 37 additions & 0 deletions tests/queries_/test_explain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import json

from bson import ObjectId, json_util
from django.test import TestCase

from .models import Author


class ExplainTests(TestCase):
def test_idented(self):
"""The JSON is dumped with indent=4."""
result = Author.objects.filter().explain()
self.assertEqual(result[:23], '{\n "explainVersion":')

def test_object_id(self):
"""
The json is dumped with bson.json_util() so that BSON types like ObjectID are
specially encoded.
"""
id = ObjectId()
result = Author.objects.filter(id=id).explain()
parsed = json_util.loads(result)
self.assertEqual(
parsed["command"]["pipeline"], [{"$match": {"$expr": {"$eq": ["$_id", id]}}}]
)

def test_non_ascii(self):
"""The json is dumped with ensure_ascii=False."""
name = "\U0001d120"
result = Author.objects.filter(name=name).explain()
# The non-decoded string must be checked since json.loads() unescapes
# non-ASCII characters.
self.assertIn(name, result)
parsed = json.loads(result)
self.assertEqual(
parsed["command"]["pipeline"], [{"$match": {"$expr": {"$eq": ["$name", name]}}}]
)