Skip to content

Commit c7b2b03

Browse files
committed
Implement explain for Find and FindOne
1 parent 2c07f8d commit c7b2b03

File tree

4 files changed

+238
-19
lines changed

4 files changed

+238
-19
lines changed

src/Operation/Explain.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use MongoDB\Driver\Server;
2222
use MongoDB\Exception\UnsupportedException;
2323
use MongoDB\Exception\InvalidArgumentException;
24+
use MongoDB\Model\BSONDocument;
2425

2526
/**
2627
* Operation for the explain command.

src/Operation/Find.php

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
use MongoDB\Driver\Exception\RuntimeException as DriverRuntimeException;
2727
use MongoDB\Exception\InvalidArgumentException;
2828
use MongoDB\Exception\UnsupportedException;
29-
29+
use MongoDB\Model\BSONDocument;
3030
/**
3131
* Operation for the find command.
3232
*
@@ -35,7 +35,7 @@
3535
* @see http://docs.mongodb.org/manual/tutorial/query-documents/
3636
* @see http://docs.mongodb.org/manual/reference/operator/query-modifier/
3737
*/
38-
class Find implements Executable
38+
class Find implements Executable, Explainable
3939
{
4040
const NON_TAILABLE = 1;
4141
const TAILABLE = 2;
@@ -284,7 +284,7 @@ public function execute(Server $server)
284284
throw UnsupportedException::readConcernNotSupported();
285285
}
286286

287-
$cursor = $server->executeQuery($this->databaseName . '.' . $this->collectionName, $this->createQuery(), $this->createOptions());
287+
$cursor = $server->executeQuery($this->databaseName . '.' . $this->collectionName, new Query($this->filter, $this->createFindOptions()), $this->createOptions());
288288

289289
if (isset($this->options['typeMap'])) {
290290
$cursor->setTypeMap($this->options['typeMap']);
@@ -293,33 +293,49 @@ public function execute(Server $server)
293293
return $cursor;
294294
}
295295

296+
public function getCommandDocument()
297+
{
298+
return $this->createCommandDocument();
299+
}
300+
296301
/**
297-
* Create options for executing the command.
298-
*
299-
* @see http://php.net/manual/en/mongodb-driver-server.executequery.php
300-
* @return array
302+
* Construct a command document for Find
301303
*/
302-
private function createOptions()
304+
private function createCommandDocument()
303305
{
304-
$options = [];
306+
$cmd = ['find' => $this->collectionName, 'filter' => new BSONDocument($this->filter)];
305307

306-
if (isset($this->options['readPreference'])) {
307-
$options['readPreference'] = $this->options['readPreference'];
308+
$options = $this->createFindOptions();
309+
310+
if (empty($options)) {
311+
return $cmd;
308312
}
309313

310-
if (isset($this->options['session'])) {
311-
$options['session'] = $this->options['session'];
314+
// maxAwaitTimeMS is a Query level option so should not be considered here
315+
unset($options['maxAwaitTimeMS']);
316+
317+
if (isset($options['cursorType'])) {
318+
if ($options['cursorType'] === self::TAILABLE) {
319+
$cmd['tailable'] = true;
320+
}
321+
if ($options['cursorType'] === self::TAILABLE_AWAIT) {
322+
$cmd['tailable'] = true;
323+
$cmd['awaitData'] = true;
324+
}
312325
}
313326

314-
return $options;
327+
return $cmd + $options;
315328
}
316329

317330
/**
318-
* Create the find query.
331+
* Create options for the find query.
319332
*
320-
* @return Query
333+
* Note that these are separate from the options for executing the command,
334+
* which are created in createOptions().
335+
*
336+
* @return array
321337
*/
322-
private function createQuery()
338+
private function createFindOptions()
323339
{
324340
$options = [];
325341

@@ -351,6 +367,27 @@ private function createQuery()
351367
$options['modifiers'] = $modifiers;
352368
}
353369

354-
return new Query($this->filter, $options);
370+
return $options;
371+
}
372+
373+
/**
374+
* Create options for executing the command.
375+
*
376+
* @see http://php.net/manual/en/mongodb-driver-server.executequery.php
377+
* @return array
378+
*/
379+
private function createOptions()
380+
{
381+
$options = [];
382+
383+
if (isset($this->options['readPreference'])) {
384+
$options['readPreference'] = $this->options['readPreference'];
385+
}
386+
387+
if (isset($this->options['session'])) {
388+
$options['session'] = $this->options['session'];
389+
}
390+
391+
return $options;
355392
}
356393
}

src/Operation/FindOne.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
* @see http://docs.mongodb.org/manual/tutorial/query-documents/
3131
* @see http://docs.mongodb.org/manual/reference/operator/query-modifier/
3232
*/
33-
class FindOne implements Executable
33+
class FindOne implements Executable, Explainable
3434
{
3535
private $find;
3636

@@ -126,4 +126,9 @@ public function execute(Server $server)
126126

127127
return ($document === false) ? null : $document;
128128
}
129+
130+
public function getCommandDocument()
131+
{
132+
return $this->find->getCommandDocument();
133+
}
129134
}

tests/Operation/ExplainFunctionalTest.php

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
namespace MongoDB\Tests\Operation;
44

5+
use MongoDB\Driver\BulkWrite;
56
use MongoDB\Operation\Count;
7+
use MongoDB\Operation\CreateCollection;
68
use MongoDB\Operation\Distinct;
79
use MongoDB\Operation\Explain;
10+
use MongoDB\Operation\Find;
811
use MongoDB\Operation\FindAndModify;
12+
use MongoDB\Operation\FindOne;
913
use MongoDB\Operation\InsertMany;
1014

1115
class ExplainFunctionalTest extends FunctionalTestCase
@@ -219,4 +223,176 @@ public function testFindAndModifyQueryPlanner()
219223
$this->assertTrue(array_key_exists('queryPlanner', $result));
220224
$this->assertFalse(array_key_exists('executionStats', $result));
221225
}
226+
227+
public function testFindAllPlansExecution()
228+
{
229+
$this->createFixtures(3);
230+
231+
$operation = new Find($this->getDatabaseName(), $this->getCollectionName(), [], ['readConcern' => $this->createDefaultReadConcern()]);
232+
233+
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_ALL_PLANS, 'typeMap' => ['root' => 'array']]);
234+
$result = $explainOperation->execute($this->getPrimaryServer());
235+
236+
$this->assertTrue(array_key_exists('queryPlanner', $result));
237+
$this->assertTrue(array_key_exists('executionStats', $result));
238+
$this->assertTrue(array_key_exists('allPlansExecution', $result['executionStats']));
239+
}
240+
241+
public function testFindDefaultVerbosity()
242+
{
243+
$this->createFixtures(3);
244+
245+
$operation = new Find($this->getDatabaseName(), $this->getCollectionName(), [], ['readConcern' => $this->createDefaultReadConcern()]);
246+
247+
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['typeMap' => ['root' => 'array']]);
248+
$result = $explainOperation->execute($this->getPrimaryServer());
249+
250+
$this->assertTrue(array_key_exists('queryPlanner', $result));
251+
$this->assertTrue(array_key_exists('executionStats', $result));
252+
$this->assertTrue(array_key_exists('allPlansExecution', $result['executionStats']));
253+
}
254+
255+
public function testFindExecutionStats()
256+
{
257+
$this->createFixtures(3);
258+
259+
$operation = new Find($this->getDatabaseName(), $this->getCollectionName(), [], ['readConcern' => $this->createDefaultReadConcern()]);
260+
261+
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_EXEC_STATS, 'typeMap' => ['root' => 'array']]);
262+
$result = $explainOperation->execute($this->getPrimaryServer());
263+
264+
$this->assertTrue(array_key_exists('queryPlanner', $result));
265+
$this->assertTrue(array_key_exists('executionStats', $result));
266+
$this->assertFalse(array_key_exists('allPlansExecution', $result['executionStats']));
267+
}
268+
269+
public function testFindQueryPlanner()
270+
{
271+
$this->createFixtures(3);
272+
273+
$operation = new Find($this->getDatabaseName(), $this->getCollectionName(), [], ['readConcern' => $this->createDefaultReadConcern()]);
274+
275+
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_QUERY, 'typeMap' => ['root' => 'array']]);
276+
$result = $explainOperation->execute($this->getPrimaryServer());
277+
278+
$this->assertTrue(array_key_exists('queryPlanner', $result));
279+
$this->assertFalse(array_key_exists('executionStats', $result));
280+
}
281+
282+
public function testFindMaxAwait()
283+
{
284+
if (version_compare($this->getServerVersion(), '3.2.0', '<')) {
285+
$this->markTestSkipped('maxAwaitTimeMS option is not supported');
286+
}
287+
288+
$maxAwaitTimeMS = 100;
289+
290+
/* Calculate an approximate pivot to use for time assertions. We will
291+
* assert that the duration of blocking responses is greater than this
292+
* value, and vice versa. */
293+
$pivot = ($maxAwaitTimeMS * 0.001) * 0.9;
294+
295+
// Create a capped collection.
296+
$databaseName = $this->getDatabaseName();
297+
$cappedCollectionName = $this->getCollectionName();
298+
$cappedCollectionOptions = [
299+
'capped' => true,
300+
'max' => 100,
301+
'size' => 1048576,
302+
];
303+
304+
$operation = new CreateCollection($databaseName, $cappedCollectionName, $cappedCollectionOptions);
305+
$operation->execute($this->getPrimaryServer());
306+
307+
// Insert documents into the capped collection.
308+
$bulkWrite = new BulkWrite(['ordered' => true]);
309+
$bulkWrite->insert(['_id' => 1]);
310+
$bulkWrite->insert(['_id' => 2]);
311+
$result = $this->manager->executeBulkWrite($this->getNamespace(), $bulkWrite);
312+
313+
$operation = new Find($databaseName, $cappedCollectionName, [], ['cursorType' => Find::TAILABLE_AWAIT, 'maxAwaitTimeMS' => $maxAwaitTimeMS]);
314+
315+
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['typeMap' => ['root' => 'array']]);
316+
$result = $explainOperation->execute($this->getPrimaryServer());
317+
318+
$this->assertTrue(array_key_exists('queryPlanner', $result));
319+
$this->assertTrue(array_key_exists('executionStats', $result));
320+
$this->assertTrue(array_key_exists('allPlansExecution', $result['executionStats']));
321+
}
322+
323+
public function testFindOneAllPlansExecution()
324+
{
325+
$this->createFixtures(1);
326+
327+
$operation = new FindOne($this->getDatabaseName(), $this->getCollectionName(), []);
328+
329+
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_ALL_PLANS, 'typeMap' => ['root' => 'array']]);
330+
$result = $explainOperation->execute($this->getPrimaryServer());
331+
332+
$this->assertTrue(array_key_exists('queryPlanner', $result));
333+
$this->assertTrue(array_key_exists('executionStats', $result));
334+
$this->assertTrue(array_key_exists('allPlansExecution', $result['executionStats']));
335+
}
336+
337+
public function testFindOneDefaultVerbosity()
338+
{
339+
$this->createFixtures(1);
340+
341+
$operation = new FindOne($this->getDatabaseName(), $this->getCollectionName(), []);
342+
343+
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['typeMap' => ['root' => 'array']]);
344+
$result = $explainOperation->execute($this->getPrimaryServer());
345+
346+
$this->assertTrue(array_key_exists('queryPlanner', $result));
347+
$this->assertTrue(array_key_exists('executionStats', $result));
348+
$this->assertTrue(array_key_exists('allPlansExecution', $result['executionStats']));
349+
}
350+
351+
public function testFindOneExecutionStats()
352+
{
353+
$this->createFixtures(1);
354+
355+
$operation = new FindOne($this->getDatabaseName(), $this->getCollectionName(), []);
356+
357+
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_EXEC_STATS, 'typeMap' => ['root' => 'array']]);
358+
$result = $explainOperation->execute($this->getPrimaryServer());
359+
360+
$this->assertTrue(array_key_exists('queryPlanner', $result));
361+
$this->assertTrue(array_key_exists('executionStats', $result));
362+
$this->assertFalse(array_key_exists('allPlansExecution', $result['executionStats']));
363+
}
364+
365+
public function testFindOneQueryPlanner()
366+
{
367+
$this->createFixtures(1);
368+
369+
$operation = new FindOne($this->getDatabaseName(), $this->getCollectionName(), []);
370+
371+
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_QUERY, 'typeMap' => ['root' => 'array']]);
372+
$result = $explainOperation->execute($this->getPrimaryServer());
373+
374+
$this->assertTrue(array_key_exists('queryPlanner', $result));
375+
$this->assertFalse(array_key_exists('executionStats', $result));
376+
}
377+
378+
/**
379+
* Create data fixtures.
380+
*
381+
* @param integer $n
382+
*/
383+
private function createFixtures($n)
384+
{
385+
$bulkWrite = new BulkWrite(['ordered' => true]);
386+
387+
for ($i = 1; $i <= $n; $i++) {
388+
$bulkWrite->insert([
389+
'_id' => $i,
390+
'x' => (object) ['foo' => 'bar'],
391+
]);
392+
}
393+
394+
$result = $this->manager->executeBulkWrite($this->getNamespace(), $bulkWrite);
395+
396+
$this->assertEquals($n, $result->getInsertedCount());
397+
}
222398
}

0 commit comments

Comments
 (0)