Skip to content

Commit 202215d

Browse files
committed
PYTHON-952 - Publish explain commands when $explain is used
1 parent 0bc75c1 commit 202215d

File tree

3 files changed

+56
-14
lines changed

3 files changed

+56
-14
lines changed

pymongo/cursor.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,6 @@ def __send_message(self, operation):
804804
publish = monitoring.enabled()
805805

806806
if operation:
807-
cmd_name = operation.name
808807
kwargs = {
809808
"read_preference": self.__read_preference,
810809
"exhaust": self.__exhaust,
@@ -821,6 +820,7 @@ def __send_message(self, operation):
821820
self.__exhaust_mgr = _SocketManager(response.socket_info,
822821
response.pool)
823822

823+
cmd_name = operation.name
824824
data = response.data
825825
cmd_duration = response.duration
826826
rqst_id = response.request_id
@@ -896,14 +896,17 @@ def __send_message(self, operation):
896896

897897
if publish:
898898
duration = (datetime.datetime.now() - start) + cmd_duration
899-
# Must publish in find / getMore command response format.
900-
res = {"cursor": {"id": doc["cursor_id"],
901-
"ns": self.__collection.full_name},
902-
"ok": 1}
903-
if cmd_name == "find":
904-
res["cursor"]["firstBatch"] = doc["data"]
899+
# Must publish in find / getMore / explain command response format.
900+
if cmd_name == "explain":
901+
res = doc["data"][0] if doc["number_returned"] else {}
905902
else:
906-
res["cursor"]["nextBatch"] = doc["data"]
903+
res = {"cursor": {"id": doc["cursor_id"],
904+
"ns": self.__collection.full_name},
905+
"ok": 1}
906+
if cmd_name == "find":
907+
res["cursor"]["firstBatch"] = doc["data"]
908+
else:
909+
res["cursor"]["nextBatch"] = doc["data"]
907910
monitoring.publish_command_success(
908911
duration, res, cmd_name, rqst_id, self.__address)
909912

pymongo/message.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ def _maybe_add_read_preference(spec, read_preference):
7575
return spec
7676

7777

78-
# XXX: What to do about exhaust?
7978
_OPTIONS = SON([
8079
('tailable', 2),
8180
('oplogReplay', 8),
@@ -84,7 +83,6 @@ def _maybe_add_read_preference(spec, read_preference):
8483
('allowPartialResults', 128)])
8584

8685

87-
# XXX: What about $explain? Time to switch to explain command?
8886
_MODIFIERS = SON([
8987
('$query', 'filter'),
9088
('$orderby', 'sort'),
@@ -101,11 +99,20 @@ def _maybe_add_read_preference(spec, read_preference):
10199
('$snapshot', 'snapshot')])
102100

103101

102+
def _gen_explain_command(
103+
coll, spec, projection, skip, limit, batch_size, options):
104+
"""Generate an explain command document."""
105+
cmd = _gen_find_command(
106+
coll, spec, projection, skip, limit, batch_size, options)
107+
return SON([('explain', cmd)])
108+
109+
104110
def _gen_find_command(coll, spec, projection, skip, limit, batch_size, options):
105111
"""Generate a find command document."""
106112
cmd = SON([('find', coll)])
107113
if '$query' in spec:
108-
cmd.update([(_MODIFIERS[key], val) for key, val in spec.items()])
114+
cmd.update([(_MODIFIERS[key], val)
115+
for key, val in spec.items() if key in _MODIFIERS])
109116
else:
110117
cmd['filter'] = spec
111118

@@ -143,9 +150,8 @@ class _Query(object):
143150
"""A query operation."""
144151

145152
__slots__ = ('flags', 'ns', 'ntoskip', 'ntoreturn', 'spec', 'fields',
146-
'codec_options', 'read_preference', 'limit', 'batch_size')
147-
148-
name = 'find'
153+
'codec_options', 'read_preference', 'limit', 'batch_size',
154+
'name')
149155

150156
def __init__(self, flags, ns, ntoskip, ntoreturn, spec, fields,
151157
codec_options, read_preference, limit, batch_size):
@@ -159,13 +165,19 @@ def __init__(self, flags, ns, ntoskip, ntoreturn, spec, fields,
159165
self.read_preference = read_preference
160166
self.limit = limit
161167
self.batch_size = batch_size
168+
self.name = 'find'
162169

163170
def as_command(self):
164171
"""Return a find command document for this query.
165172
166173
Should be called *after* get_message.
167174
"""
168175
dbn, coll = self.ns.split('.', 1)
176+
if '$explain' in self.spec:
177+
self.name = 'explain'
178+
return _gen_explain_command(
179+
coll, self.spec, self.fields, self.ntoskip,
180+
self.limit, self.batch_size, self.flags), dbn
169181
return _gen_find_command(coll, self.spec, self.fields, self.ntoskip,
170182
self.limit, self.batch_size, self.flags), dbn
171183

test/test_monitoring.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,33 @@ def test_find_and_get_more(self):
198198
'ok': 1}
199199
self.assertEqual(expected_result, succeeded.reply)
200200

201+
def test_find_with_explain(self):
202+
self.client.pymongo_test.test.drop()
203+
self.client.pymongo_test.test.insert_one({})
204+
self.listener.results = {}
205+
res = self.client.pymongo_test.test.find().explain()
206+
results = self.listener.results
207+
started = results.get('started')
208+
succeeded = results.get('succeeded')
209+
self.assertIsNone(results.get('failed'))
210+
self.assertTrue(
211+
isinstance(started, monitoring.CommandStartedEvent))
212+
self.assertEqual(
213+
SON([('explain', SON([('find', 'test'),
214+
('filter', {})]))]),
215+
started.command)
216+
self.assertEqual('explain', started.command_name)
217+
self.assertEqual(self.client.address, started.connection_id)
218+
self.assertEqual('pymongo_test', started.database_name)
219+
self.assertTrue(isinstance(started.request_id, int))
220+
self.assertTrue(
221+
isinstance(succeeded, monitoring.CommandSucceededEvent))
222+
self.assertTrue(isinstance(succeeded.duration_micros, int))
223+
self.assertEqual('explain', succeeded.command_name)
224+
self.assertTrue(isinstance(succeeded.request_id, int))
225+
self.assertEqual(self.client.address, succeeded.connection_id)
226+
self.assertEqual(res, succeeded.reply)
227+
201228
@client_context.require_version_min(2, 6, 0)
202229
def test_command_and_get_more(self):
203230
self.client.pymongo_test.test.drop()

0 commit comments

Comments
 (0)