Commit ce17e9a8 authored by Katherine Walker's avatar Katherine Walker

Create Explain operation class

parent 0ad32023
...@@ -34,7 +34,7 @@ use MongoDB\Exception\UnsupportedException; ...@@ -34,7 +34,7 @@ use MongoDB\Exception\UnsupportedException;
* @see \MongoDB\Collection::count() * @see \MongoDB\Collection::count()
* @see http://docs.mongodb.org/manual/reference/command/count/ * @see http://docs.mongodb.org/manual/reference/command/count/
*/ */
class Count implements Explainable class Count implements Executable, Explainable
{ {
private static $wireVersionForCollation = 5; private static $wireVersionForCollation = 5;
private static $wireVersionForReadConcern = 4; private static $wireVersionForReadConcern = 4;
...@@ -162,37 +162,9 @@ class Count implements Explainable ...@@ -162,37 +162,9 @@ class Count implements Explainable
return (integer) $result->n; return (integer) $result->n;
} }
/** public function getCommandDocument()
* Explain the operation.
*
* @see Explainable::explain()
* @param Server $server
* @param $command
* @param array $options
* @return array|object
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
public function explain($server, $command, $options = [])
{ {
if ( ! isset($options['verbosity'])) { return $this->createCommandDocument();
$options['verbosity'] = 'allPlansExecution';
}
$cmd = new Command(['explain' => ['count' => $this->collectionName, 'query' => $command], 'verbosity' => $options['verbosity']]);
if (empty($command)) {
$cmd = new Command(['explain' => ['count' => $this->collectionName], 'verbosity' => $options['verbosity']]);
}
$result = $server->executeCommand($this->databaseName, $cmd);
$resultArray = get_object_vars($result->toArray()[0]); // cast $result to array
if ($options['verbosity'] === 'queryPlanner') {
return ['queryPlanner' => $resultArray['queryPlanner']];
}
return ['queryPlanner' => $resultArray['queryPlanner'], 'executionStats' => $resultArray['executionStats']];
} }
/** /**
...@@ -201,6 +173,16 @@ class Count implements Explainable ...@@ -201,6 +173,16 @@ class Count implements Explainable
* @return Command * @return Command
*/ */
private function createCommand() private function createCommand()
{
return new Command($this->createCommandDocument());
}
/**
* Create the count command document.
*
* @return array
*/
private function createCommandDocument()
{ {
$cmd = ['count' => $this->collectionName]; $cmd = ['count' => $this->collectionName];
...@@ -222,7 +204,7 @@ class Count implements Explainable ...@@ -222,7 +204,7 @@ class Count implements Explainable
} }
} }
return new Command($cmd); return $cmd;
} }
/** /**
......
...@@ -34,7 +34,7 @@ use MongoDB\Exception\UnsupportedException; ...@@ -34,7 +34,7 @@ use MongoDB\Exception\UnsupportedException;
* @see \MongoDB\Collection::distinct() * @see \MongoDB\Collection::distinct()
* @see http://docs.mongodb.org/manual/reference/command/distinct/ * @see http://docs.mongodb.org/manual/reference/command/distinct/
*/ */
class Distinct implements Executable class Distinct implements Executable, Explainable
{ {
private static $wireVersionForCollation = 5; private static $wireVersionForCollation = 5;
private static $wireVersionForReadConcern = 4; private static $wireVersionForReadConcern = 4;
...@@ -143,12 +143,27 @@ class Distinct implements Executable ...@@ -143,12 +143,27 @@ class Distinct implements Executable
return $result->values; return $result->values;
} }
public function getCommandDocument()
{
return $this->createCommandDocument();
}
/** /**
* Create the distinct command. * Create the distinct command.
* *
* @return Command * @return Command
*/ */
private function createCommand() private function createCommand()
{
return new Command($this->createCommandDocument());
}
/**
* Create the distinct command document.
*
* @return array
*/
private function createCommandDocument()
{ {
$cmd = [ $cmd = [
'distinct' => $this->collectionName, 'distinct' => $this->collectionName,
...@@ -167,7 +182,7 @@ class Distinct implements Executable ...@@ -167,7 +182,7 @@ class Distinct implements Executable
$cmd['maxTimeMS'] = $this->options['maxTimeMS']; $cmd['maxTimeMS'] = $this->options['maxTimeMS'];
} }
return new Command($cmd); return $cmd;
} }
/** /**
......
<?php
/*
* Copyright 2018 MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
namespace MongoDB\Operation;
use MongoDB\Driver\Command;
use MongoDB\Driver\Server;
/**
* Operation for the explain command.
*
* @api
* @see \MongoDB\Collection::explain()
* @see http://docs.mongodb.org/manual/reference/command/explain/
*/
class Explain implements Executable
{
const VERBOSITY_ALL_PLANS = 'allPlansExecution';
const VERBOSITY_EXEC_STATS = 'executionStats';
const VERBOSITY_QUERY = 'queryPlanner';
private $databaseName;
private $explainable;
private $options;
/* The Explainable knows what collection it targets, so we only need
* a database to run `explain` on. Alternatively, we might decide to also
* pull the database from Explainable somehow, since all Operations we
* might explain also require a database name.
*
* Options will at least be verbosity (the only documented explain option)
* and typeMap, which we can apply to the cursor before the execute()
* method returns current($cursor->toArray()) or $cursor->toArray()[0]
* (both are equivalent). */
public function __construct($databaseName, Explainable $explainable, array $options = [])
{
$this->databaseName = $databaseName;
$this->explainable = $explainable;
$this->options = $options;
}
public function execute(Server $server)
{
$cmd = ['explain' => $this->explainable->getCommandDocument()];
if (isset($this->options['verbosity'])) {
$cmd['verbosity'] = $this->options['verbosity'];
}
$cursor = $server->executeCommand($this->databaseName, new Command($cmd));
if (isset($this->options['typeMap'])) {
$cursor->setTypeMap($this->options['typeMap']);
}
return current($cursor->toArray());
}
}
...@@ -20,19 +20,12 @@ namespace MongoDB\Operation; ...@@ -20,19 +20,12 @@ namespace MongoDB\Operation;
use MongoDB\Driver\Server; use MongoDB\Driver\Server;
/** /**
* Explainable interface for explainable operations (count, distinct, group, * Explainable interface for explainable operations (count, distinct, find,
* find, findAndModify, delete, and update). * findAndModify, delete, and update).
* *
* @internal * @internal
*/ */
interface Explainable extends Executable interface Explainable extends Executable
{ {
/** function getCommandDocument();
* Explain the operation.
*
* @param $command
* @param array $options
* @return mixed
*/
public function explain($server, $command, $options = []);
} }
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
namespace MongoDB\Operation; namespace MongoDB\Operation;
use MongoDB\Driver\Command;
use MongoDB\Driver\Cursor; use MongoDB\Driver\Cursor;
use MongoDB\Driver\Query; use MongoDB\Driver\Query;
use MongoDB\Driver\ReadConcern; use MongoDB\Driver\ReadConcern;
...@@ -36,7 +35,7 @@ use MongoDB\Exception\UnsupportedException; ...@@ -36,7 +35,7 @@ use MongoDB\Exception\UnsupportedException;
* @see http://docs.mongodb.org/manual/tutorial/query-documents/ * @see http://docs.mongodb.org/manual/tutorial/query-documents/
* @see http://docs.mongodb.org/manual/reference/operator/query-modifier/ * @see http://docs.mongodb.org/manual/reference/operator/query-modifier/
*/ */
class Find implements Explainable class Find implements Executable
{ {
const NON_TAILABLE = 1; const NON_TAILABLE = 1;
const TAILABLE = 2; const TAILABLE = 2;
...@@ -294,38 +293,6 @@ class Find implements Explainable ...@@ -294,38 +293,6 @@ class Find implements Explainable
return $cursor; return $cursor;
} }
/**
* Explain the operation.
*
* @see Explainable::explain()
* @param Server $server
* @param $command
* @param array $options
* @return array|object
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
*/
public function explain($server, $command, $options = [])
{
if ( ! isset($options['verbosity'])) {
$options['verbosity'] = 'allPlansExecution';
}
$cmd = new Command(['explain' => ['find' => $this->collectionName, 'query' => $command], 'verbosity' => $options['verbosity']]);
if (empty($command)) {
$cmd = new Command(['explain' => ['find' => $this->collectionName], 'verbosity' => $options['verbosity']]);
}
$result = $server->executeCommand($this->databaseName, $cmd);
$resultArray = get_object_vars($result->toArray()[0]);
if ($options['verbosity'] === 'queryPlanner') {
return ['queryPlanner' => $resultArray['queryPlanner']];
}
return ['queryPlanner' => $resultArray['queryPlanner'], 'executionStats' => $resultArray['executionStats']];
}
/** /**
* Create options for executing the command. * Create options for executing the command.
* *
......
...@@ -35,7 +35,7 @@ use MongoDB\Exception\UnsupportedException; ...@@ -35,7 +35,7 @@ use MongoDB\Exception\UnsupportedException;
* @internal * @internal
* @see http://docs.mongodb.org/manual/reference/command/findAndModify/ * @see http://docs.mongodb.org/manual/reference/command/findAndModify/
*/ */
class FindAndModify implements Executable class FindAndModify implements Executable, Explainable
{ {
private static $wireVersionForArrayFilters = 6; private static $wireVersionForArrayFilters = 6;
private static $wireVersionForCollation = 5; private static $wireVersionForCollation = 5;
...@@ -239,6 +239,11 @@ class FindAndModify implements Executable ...@@ -239,6 +239,11 @@ class FindAndModify implements Executable
return $result->value; return $result->value;
} }
public function getCommandDocument()
{
return $this->createCommandDocument();
}
/** /**
* Create the findAndModify command. * Create the findAndModify command.
* *
...@@ -246,6 +251,16 @@ class FindAndModify implements Executable ...@@ -246,6 +251,16 @@ class FindAndModify implements Executable
* @return Command * @return Command
*/ */
private function createCommand(Server $server) private function createCommand(Server $server)
{
return new Command($this->createCommandDocument());
}
/**
* Create the findAndModify command document.
*
* @return array
*/
private function createCommandDocument()
{ {
$cmd = ['findAndModify' => $this->collectionName]; $cmd = ['findAndModify' => $this->collectionName];
...@@ -274,7 +289,7 @@ class FindAndModify implements Executable ...@@ -274,7 +289,7 @@ class FindAndModify implements Executable
$cmd['bypassDocumentValidation'] = $this->options['bypassDocumentValidation']; $cmd['bypassDocumentValidation'] = $this->options['bypassDocumentValidation'];
} }
return new Command($cmd); return $cmd;
} }
/** /**
......
...@@ -4,6 +4,7 @@ namespace MongoDB\Tests\Operation; ...@@ -4,6 +4,7 @@ namespace MongoDB\Tests\Operation;
use MongoDB\Operation\Count; use MongoDB\Operation\Count;
use MongoDB\Operation\CreateIndexes; use MongoDB\Operation\CreateIndexes;
use MongoDB\Operation\Explain;
use MongoDB\Operation\InsertMany; use MongoDB\Operation\InsertMany;
use MongoDB\Tests\CommandObserver; use MongoDB\Tests\CommandObserver;
use stdClass; use stdClass;
...@@ -39,10 +40,31 @@ class CountFunctionalTest extends FunctionalTestCase ...@@ -39,10 +40,31 @@ class CountFunctionalTest extends FunctionalTestCase
]); ]);
$insertMany->execute($this->getPrimaryServer()); $insertMany->execute($this->getPrimaryServer());
$operation = new Count($this->getDatabaseName(), $this->getCollectionName(), [], []); $operation = new Count($this->getDatabaseName(), $this->getCollectionName(), ['x' => ['$gte' => 1]], []);
$result = $operation->explain($this->getPrimaryServer(), ['x' => ['$gte' => 1]]); $explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_ALL_PLANS, 'typeMap' => ['root' => 'array']]);
$result = $explainOperation->execute($this->getPrimaryServer());
$this->assertSame(['queryPlanner', 'executionStats'], array_keys($result)); $this->assertTrue(array_key_exists('queryPlanner', $result));
$this->assertTrue(array_key_exists('executionStats', $result));
$this->assertTrue(array_key_exists('allPlansExecution', $result['executionStats']));
}
public function testExplainDefaultVerbosity()
{
$insertMany = new InsertMany($this->getDatabaseName(), $this->getCollectionName(), [
['x' => 0],
['x' => 1],
['x' => 2],
['y' => 3]
]);
$insertMany->execute($this->getPrimaryServer());
$operation = new Count($this->getDatabaseName(), $this->getCollectionName(), ['x' => ['$gte' => 1]], []);
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['typeMap' => ['root' => 'array']]);
$result = $explainOperation->execute($this->getPrimaryServer());
$this->assertTrue(array_key_exists('queryPlanner', $result));
$this->assertTrue(array_key_exists('executionStats', $result));
$this->assertTrue(array_key_exists('allPlansExecution', $result['executionStats'])); $this->assertTrue(array_key_exists('allPlansExecution', $result['executionStats']));
} }
...@@ -56,10 +78,13 @@ class CountFunctionalTest extends FunctionalTestCase ...@@ -56,10 +78,13 @@ class CountFunctionalTest extends FunctionalTestCase
]); ]);
$insertMany->execute($this->getPrimaryServer()); $insertMany->execute($this->getPrimaryServer());
$operation = new Count($this->getDatabaseName(), $this->getCollectionName(), [], []); $operation = new Count($this->getDatabaseName(), $this->getCollectionName(), ['x' => ['$gte' => 1]], []);
$result = $operation->explain($this->getPrimaryServer(), ['x' => ['$gte' => 1]], ['verbosity' => 'executionStats']); $explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_EXEC_STATS, 'typeMap' => ['root' => 'array']]);
$result = $explainOperation->execute($this->getPrimaryServer());
$this->assertTrue(array_key_exists('queryPlanner', $result));
$this->assertTrue(array_key_exists('executionStats', $result));
$this->assertSame(['queryPlanner', 'executionStats'], array_keys($result));
$this->assertFalse(array_key_exists('allPlansExecution', $result['executionStats'])); $this->assertFalse(array_key_exists('allPlansExecution', $result['executionStats']));
} }
...@@ -73,10 +98,12 @@ class CountFunctionalTest extends FunctionalTestCase ...@@ -73,10 +98,12 @@ class CountFunctionalTest extends FunctionalTestCase
]); ]);
$insertMany->execute($this->getPrimaryServer()); $insertMany->execute($this->getPrimaryServer());
$operation = new Count($this->getDatabaseName(), $this->getCollectionName(), [], []); $operation = new Count($this->getDatabaseName(), $this->getCollectionName(), ['x' => ['$gte' => 1]], []);
$result = $operation->explain($this->getPrimaryServer(), ['x' => ['$gte' => 1]], ['verbosity' => 'queryPlanner']); $explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_QUERY, 'typeMap' => ['root' => 'array']]);
$result = $explainOperation->execute($this->getPrimaryServer());
$this->assertSame(['queryPlanner'], array_keys($result)); $this->assertTrue(array_key_exists('queryPlanner', $result));
$this->assertFalse(array_key_exists('executionStats', $result));
} }
public function testHintOption() public function testHintOption()
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
namespace MongoDB\Tests\Operation; namespace MongoDB\Tests\Operation;
use MongoDB\Operation\Distinct; use MongoDB\Operation\Distinct;
use MongoDB\Operation\Explain;
use MongoDB\Tests\CommandObserver; use MongoDB\Tests\CommandObserver;
use stdClass; use stdClass;
...@@ -28,6 +29,53 @@ class DistinctFunctionalTest extends FunctionalTestCase ...@@ -28,6 +29,53 @@ class DistinctFunctionalTest extends FunctionalTestCase
); );
} }
public function testExplainAllPlansExecution()
{
$operation = new Distinct($this->getDatabaseName(), $this->getCollectionName(), 'x', []);
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_ALL_PLANS, 'typeMap' => ['root' => 'array']]);
$result = $explainOperation->execute($this->getPrimaryServer());
$this->assertTrue(array_key_exists('queryPlanner', $result));
$this->assertTrue(array_key_exists('executionStats', $result));
$this->assertTrue(array_key_exists('allPlansExecution', $result['executionStats']));
}
public function testExplainDefaultVerbosity()
{
$operation = new Distinct($this->getDatabaseName(), $this->getCollectionName(), 'x', []);
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['typeMap' => ['root' => 'array']]);
$result = $explainOperation->execute($this->getPrimaryServer());
$this->assertTrue(array_key_exists('queryPlanner', $result));
$this->assertTrue(array_key_exists('executionStats', $result));
$this->assertTrue(array_key_exists('allPlansExecution', $result['executionStats']));
}
public function testExplainExecutionStats()
{
$operation = new Distinct($this->getDatabaseName(), $this->getCollectionName(), 'x', []);
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_EXEC_STATS, 'typeMap' => ['root' => 'array']]);
$result = $explainOperation->execute($this->getPrimaryServer());
$this->assertTrue(array_key_exists('queryPlanner', $result));
$this->assertTrue(array_key_exists('executionStats', $result));
$this->assertFalse(array_key_exists('allPlansExecution', $result['executionStats']));
}
public function testExplainQueryPlanner()
{
$operation = new Distinct($this->getDatabaseName(), $this->getCollectionName(), 'x', []);
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_QUERY, 'typeMap' => ['root' => 'array']]);
$result = $explainOperation->execute($this->getPrimaryServer());
$this->assertTrue(array_key_exists('queryPlanner', $result));
$this->assertFalse(array_key_exists('executionStats', $result));
}
public function testSessionOption() public function testSessionOption()
{ {
if (version_compare($this->getServerVersion(), '3.6.0', '<')) { if (version_compare($this->getServerVersion(), '3.6.0', '<')) {
......
...@@ -4,6 +4,7 @@ namespace MongoDB\Tests\Operation; ...@@ -4,6 +4,7 @@ namespace MongoDB\Tests\Operation;
use MongoDB\Driver\BulkWrite; use MongoDB\Driver\BulkWrite;
use MongoDB\Model\BSONDocument; use MongoDB\Model\BSONDocument;
use MongoDB\Operation\Explain;
use MongoDB\Operation\FindAndModify; use MongoDB\Operation\FindAndModify;
use MongoDB\Tests\CommandObserver; use MongoDB\Tests\CommandObserver;
use stdClass; use stdClass;
...@@ -28,6 +29,53 @@ class FindAndModifyFunctionalTest extends FunctionalTestCase ...@@ -28,6 +29,53 @@ class FindAndModifyFunctionalTest extends FunctionalTestCase
); );
} }
public function testExplainAllPlansExecution()
{
$operation = new FindAndModify($this->getDatabaseName(), $this->getCollectionName(), ['remove' => true, 'session' => $this->createSession()]);
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_ALL_PLANS, 'typeMap' => ['root' => 'array']]);
$result = $explainOperation->execute($this->getPrimaryServer());
$this->assertTrue(array_key_exists('queryPlanner', $result));
$this->assertTrue(array_key_exists('executionStats', $result));
$this->assertTrue(array_key_exists('allPlansExecution', $result['executionStats']));
}
public function testExplainDefaultVerbosity()
{
$operation = new FindAndModify($this->getDatabaseName(), $this->getCollectionName(), ['remove' => true, 'session' => $this->createSession()]);
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['typeMap' => ['root' => 'array']]);
$result = $explainOperation->execute($this->getPrimaryServer());
$this->assertTrue(array_key_exists('queryPlanner', $result));
$this->assertTrue(array_key_exists('executionStats', $result));
$this->assertTrue(array_key_exists('allPlansExecution', $result['executionStats']));
}
public function testExplainExecutionStats()
{
$operation = new FindAndModify($this->getDatabaseName(), $this->getCollectionName(), ['remove' => true, 'session' => $this->createSession()]);
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_EXEC_STATS, 'typeMap' => ['root' => 'array']]);
$result = $explainOperation->execute($this->getPrimaryServer());
$this->assertTrue(array_key_exists('queryPlanner', $result));
$this->assertTrue(array_key_exists('executionStats', $result));
$this->assertFalse(array_key_exists('allPlansExecution', $result['executionStats']));
}
public function testExplainQueryPlanner()
{
$operation = new FindAndModify($this->getDatabaseName(), $this->getCollectionName(), ['remove' => true, 'session' => $this->createSession()]);
$explainOperation = new Explain($this->getDatabaseName(), $operation, ['verbosity' => Explain::VERBOSITY_QUERY, 'typeMap' => ['root' => 'array']]);
$result = $explainOperation->execute($this->getPrimaryServer());
$this->assertTrue(array_key_exists('queryPlanner', $result));
$this->assertFalse(array_key_exists('executionStats', $result));
}
public function testSessionOption() public function testSessionOption()
{ {
if (version_compare($this->getServerVersion(), '3.6.0', '<')) { if (version_compare($this->getServerVersion(), '3.6.0', '<')) {
......
...@@ -217,41 +217,6 @@ class FindFunctionalTest extends FunctionalTestCase ...@@ -217,41 +217,6 @@ class FindFunctionalTest extends FunctionalTestCase
$this->assertFalse($it->valid()); $this->assertFalse($it->valid());
} }
public function testExplainAllPlansExecution()
{
$this->createFixtures(3);
$operation = new Find($this->getDatabaseName(), $this->getCollectionName(), [], []);
$command = [];
$result = $operation->explain($this->getPrimaryServer(), $command);
$this->assertSame(['queryPlanner', 'executionStats'], array_keys($result));
$this->assertTrue(array_key_exists('allPlansExecution', $result['executionStats']));
}
public function testExplainExecutionStats()
{
$this->createFixtures(3);
$operation = new Find($this->getDatabaseName(), $this->getCollectionName(), [], []);
$command = [];
$result = $operation->explain($this->getPrimaryServer(), $command, ['verbosity' => 'executionStats']);
$this->assertSame(['queryPlanner', 'executionStats'], array_keys($result));
$this->assertFalse(array_key_exists('allPlansExecution', $result['executionStats']));
}
public function testExplainQueryPlanner()
{
$this->createFixtures(3);
$operation = new Find($this->getDatabaseName(), $this->getCollectionName(), [], []);
$command = [];
$result = $operation->explain($this->getPrimaryServer(), $command, ['verbosity' => 'queryPlanner']);
$this->assertSame(['queryPlanner'], array_keys($result));
}
/** /**
* Create data fixtures. * Create data fixtures.
* *
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment