Skip to content

Commit 6b5d00c

Browse files
Jibolatimgraham
andauthored
Make QuerySet.explain() return parsable JSON (#340)
Co-authored-by: Tim Graham <[email protected]>
1 parent 880c906 commit 6b5d00c

File tree

4 files changed

+61
-8
lines changed

4 files changed

+61
-8
lines changed

django_mongodb_backend/compiler.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import itertools
2-
import pprint
32
from collections import defaultdict
43

5-
from bson import SON
4+
from bson import SON, json_util
65
from django.core.exceptions import EmptyResultSet, FieldError, FullResultSet
76
from django.db import IntegrityError, NotSupportedError
87
from django.db.models import Count
@@ -652,12 +651,7 @@ def explain_query(self):
652651
{"aggregate": self.collection_name, "pipeline": pipeline, "cursor": {}},
653652
**kwargs,
654653
)
655-
# Generate the output: a list of lines that Django joins with newlines.
656-
result = []
657-
for key, value in explain.items():
658-
formatted_value = pprint.pformat(value, indent=4)
659-
result.append(f"{key}: {formatted_value}")
660-
return result
654+
return [json_util.dumps(explain, indent=4, ensure_ascii=False)]
661655

662656

663657
class SQLInsertCompiler(SQLCompiler):

docs/source/ref/models/querysets.rst

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ In addition, :meth:`QuerySet.delete() <django.db.models.query.QuerySet.delete>`
1515
and :meth:`update() <django.db.models.query.QuerySet.update>` do not support
1616
queries that span multiple collections.
1717

18+
.. _queryset-explain:
19+
1820
``QuerySet.explain()``
1921
======================
2022

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

34+
The result of ``explain()`` is a string::
35+
36+
>>> print(Model.objects.explain())
37+
{
38+
"explainVersion": "1",
39+
"queryPlanner": {
40+
...
41+
},
42+
...
43+
}
44+
45+
that can be parsed as JSON::
46+
47+
>>> from bson import json_util
48+
>>> result = Model.objects.filter(name="MongoDB").explain()
49+
>>> json_util.loads(result)['command']["pipeline"]
50+
[{'$match': {'$expr': {'$eq': ['$name', 'MongoDB']}}}]
51+
3252
MongoDB-specific ``QuerySet`` methods
3353
=====================================
3454

docs/source/releases/5.2.x.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ Bug fixes
2222

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

2628
5.2.0 beta 1
2729
============

tests/queries_/test_explain.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import json
2+
3+
from bson import ObjectId, json_util
4+
from django.test import TestCase
5+
6+
from .models import Author
7+
8+
9+
class ExplainTests(TestCase):
10+
def test_idented(self):
11+
"""The JSON is dumped with indent=4."""
12+
result = Author.objects.filter().explain()
13+
self.assertEqual(result[:23], '{\n "explainVersion":')
14+
15+
def test_object_id(self):
16+
"""
17+
The json is dumped with bson.json_util() so that BSON types like ObjectID are
18+
specially encoded.
19+
"""
20+
id = ObjectId()
21+
result = Author.objects.filter(id=id).explain()
22+
parsed = json_util.loads(result)
23+
self.assertEqual(
24+
parsed["command"]["pipeline"], [{"$match": {"$expr": {"$eq": ["$_id", id]}}}]
25+
)
26+
27+
def test_non_ascii(self):
28+
"""The json is dumped with ensure_ascii=False."""
29+
name = "\U0001d120"
30+
result = Author.objects.filter(name=name).explain()
31+
# The non-decoded string must be checked since json.loads() unescapes
32+
# non-ASCII characters.
33+
self.assertIn(name, result)
34+
parsed = json.loads(result)
35+
self.assertEqual(
36+
parsed["command"]["pipeline"], [{"$match": {"$expr": {"$eq": ["$name", name]}}}]
37+
)

0 commit comments

Comments
 (0)