Commit ce17e9a8 authored by Katherine Walker's avatar Katherine Walker

Create Explain operation class

parent 0ad32023
......@@ -34,7 +34,7 @@ use MongoDB\Exception\UnsupportedException;
* @see \MongoDB\Collection::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 $wireVersionForReadConcern = 4;
......@@ -162,37 +162,9 @@ class Count implements Explainable
return (integer) $result->n;
}
/**
* 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 = [])
public function getCommandDocument()
{
if ( ! isset($options['verbosity'])) {
$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']];
return $this->createCommandDocument();
}
/**
......@@ -201,6 +173,16 @@ class Count implements Explainable
* @return Command
*/
private function createCommand()
{
return new Command($this->createCommandDocument());
}
/**
* Create the count command document.
*
* @return array
*/
private function createCommandDocument()
{
$cmd = ['count' => $this->collectionName];
......@@ -222,7 +204,7 @@ class Count implements Explainable
}
}
return new Command($cmd);
return $cmd;
}
/**
......
......@@ -34,7 +34,7 @@ use MongoDB\Exception\UnsupportedException;
* @see \MongoDB\Collection::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 $wireVersionForReadConcern = 4;
......@@ -143,12 +143,27 @@ class Distinct implements Executable
return $result->values;
}
public function getCommandDocument()
{
return $this->createCommandDocument();
}
/**
* Create the distinct command.
*
* @return Command
*/
private function createCommand()
{
return new Command($this->createCommandDocument());
}
/**
* Create the distinct command document.
*
* @return array
*/
private function createCommandDocument()
{
$cmd = [
'distinct' => $this->collectionName,
......@@ -167,7 +182,7 @@ class Distinct implements Executable
$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;
use MongoDB\Driver\Server;
/**
* Explainable interface for explainable operations (count, distinct, group,
* find, findAndModify, delete, and update).
* Explainable interface for explainable operations (count, distinct, find,
* findAndModify, delete, and update).
*
* @internal
*/
interface Explainable extends Executable
{
/**
* Explain the operation.
*
* @param $command
* @param array $options
* @return mixed
*/
public function explain($server, $command, $options = []);
function getCommandDocument();
}
......@@ -17,7 +17,6 @@
namespace MongoDB\Operation;
use MongoDB\Driver\Command;
use MongoDB\Driver\Cursor;
use MongoDB\Driver\Query;
use MongoDB\Driver\ReadConcern;
......@@ -36,7 +35,7 @@ use MongoDB\Exception\UnsupportedException;
* @see http://docs.mongodb.org/manual/tutorial/query-documents/
* @see http://docs.mongodb.org/manual/reference/operator/query-modifier/
*/
class Find implements Explainable
class Find implements Executable
{
const NON_TAILABLE = 1;
const TAILABLE = 2;
......@@ -294,38 +293,6 @@ class Find implements Explainable
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.
*
......
......@@ -35,7 +35,7 @@ use MongoDB\Exception\UnsupportedException;
* @internal
* @see http://docs.mongodb.org/manual/reference/command/findAndModify/
*/
class FindAndModify implements Executable
class FindAndModify implements Executable, Explainable
{
private static $wireVersionForArrayFilters = 6;
private static $wireVersionForCollation = 5;
......@@ -239,6 +239,11 @@ class FindAndModify implements Executable
return $result->value;
}
public function getCommandDocument()
{
return $this->createCommandDocument();
}
/**
* Create the findAndModify command.
*
......@@ -246,6 +251,16 @@ class FindAndModify implements Executable
* @return Command
*/
private function createCommand(Server $server)
{
return new Command($this->createCommandDocument());
}
/**
* Create the findAndModify command document.
*
* @return array
*/
private function createCommandDocument()
{
$cmd = ['findAndModify' => $this->collectionName];
......@@ -274,7 +289,7 @@ class FindAndModify implements Executable
$cmd['bypassDocumentValidation'] = $this->options['bypassDocumentValidation'];
}
return new Command($cmd);
return $cmd;
}
/**
......
......@@ -4,6 +4,7 @@ namespace MongoDB\Tests\Operation;
use MongoDB\Operation\Count;
use MongoDB\Operation\CreateIndexes;
use MongoDB\Operation\Explain;
use MongoDB\Operation\InsertMany;
use MongoDB\Tests\CommandObserver;
use stdClass;
......@@ -39,10 +40,31 @@ class CountFunctionalTest extends FunctionalTestCase
]);
$insertMany->execute($this->getPrimaryServer());
$operation = new Count($this->getDatabaseName(), $this->getCollectionName(), [], []);
$result = $operation->explain($this->getPrimaryServer(), ['x' => ['$gte' => 1]]);
$operation = new Count($this->getDatabaseName(), $this->getCollectionName(), ['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']));
}
......@@ -56,10 +78,13 @@ class CountFunctionalTest extends FunctionalTestCase
]);
$insertMany->execute($this->getPrimaryServer());
$operation = new Count($this->getDatabaseName(), $this->getCollectionName(), [], []);
$result = $operation->explain($this->getPrimaryServer(), ['x' => ['$gte' => 1]], ['verbosity' => 'executionStats']);
$operation = new Count($this->getDatabaseName(), $this->getCollectionName(), ['x' => ['$gte' => 1]], []);
$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']));
}
......@@ -73,10 +98,12 @@ class CountFunctionalTest extends FunctionalTestCase
]);
$insertMany->execute($this->getPrimaryServer());
$operation = new Count($this->getDatabaseName(), $this->getCollectionName(), [], []);
$result = $operation->explain($this->getPrimaryServer(), ['x' => ['$gte' => 1]], ['verbosity' => 'queryPlanner']);
$operation = new Count($this->getDatabaseName(), $this->getCollectionName(), ['x' => ['$gte' => 1]], []);
$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()
......
......@@ -3,6 +3,7 @@
namespace MongoDB\Tests\Operation;
use MongoDB\Operation\Distinct;
use MongoDB\Operation\Explain;
use MongoDB\Tests\CommandObserver;
use stdClass;
......@@ -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()
{
if (version_compare($this->getServerVersion(), '3.6.0', '<')) {
......
......@@ -4,6 +4,7 @@ namespace MongoDB\Tests\Operation;
use MongoDB\Driver\BulkWrite;
use MongoDB\Model\BSONDocument;
use MongoDB\Operation\Explain;
use MongoDB\Operation\FindAndModify;
use MongoDB\Tests\CommandObserver;
use stdClass;
......@@ -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()
{
if (version_compare($this->getServerVersion(), '3.6.0', '<')) {
......
......@@ -217,41 +217,6 @@ class FindFunctionalTest extends FunctionalTestCase
$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.
*
......
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