From e0a7dfa2d319e97adef5e73eb9f977d522c9674b Mon Sep 17 00:00:00 2001 From: Jeremy Mikola Date: Wed, 6 Nov 2024 16:23:57 -0500 Subject: [PATCH] PHPLIB-1582: Support hint option for distinct command This is the first use of is_document() for validating a "hint" option. Future work will be handled in PHPLIB-1587, which can also standardize error messaging. --- src/Operation/Distinct.php | 18 ++++++++- tests/Operation/DistinctFunctionalTest.php | 44 ++++++++++++++++++++++ tests/Operation/DistinctTest.php | 3 ++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/src/Operation/Distinct.php b/src/Operation/Distinct.php index 171a18269..a979df229 100644 --- a/src/Operation/Distinct.php +++ b/src/Operation/Distinct.php @@ -31,6 +31,7 @@ use function is_array; use function is_integer; use function is_object; +use function is_string; use function MongoDB\create_field_path_type_map; use function MongoDB\is_document; @@ -53,7 +54,13 @@ class Distinct implements Executable, Explainable * * * comment (mixed): BSON value to attach as a comment to this command. * - * This is not supported for servers versions < 4.4. + * This is not supported for server versions < 4.4. + * + * * hint (string|document): The index to use. Specify either the index + * name as a string or the index key pattern as a document. If specified, + * then the query system will only consider plans using the hinted index. + * + * This is not supported for server versions < 7.1. * * * maxTimeMS (integer): The maximum amount of time to allow the query to * run. @@ -83,6 +90,10 @@ public function __construct(private string $databaseName, private string $collec throw InvalidArgumentException::expectedDocumentType('"collation" option', $this->options['collation']); } + if (isset($this->options['hint']) && ! is_string($this->options['hint']) && ! is_document($this->options['hint'])) { + throw InvalidArgumentException::invalidType('"hint" option', $this->options['hint'], 'string or array or object'); + } + if (isset($this->options['maxTimeMS']) && ! is_integer($this->options['maxTimeMS'])) { throw InvalidArgumentException::invalidType('"maxTimeMS" option', $this->options['maxTimeMS'], 'integer'); } @@ -175,6 +186,11 @@ private function createCommandDocument(): array $cmd['collation'] = (object) $this->options['collation']; } + if (isset($this->options['hint'])) { + /** @psalm-var string|object */ + $cmd['hint'] = is_array($this->options['hint']) ? (object) $this->options['hint'] : $this->options['hint']; + } + foreach (['comment', 'maxTimeMS'] as $option) { if (isset($this->options[$option])) { $cmd[$option] = $this->options[$option]; diff --git a/tests/Operation/DistinctFunctionalTest.php b/tests/Operation/DistinctFunctionalTest.php index 0a1d61a01..5106f881d 100644 --- a/tests/Operation/DistinctFunctionalTest.php +++ b/tests/Operation/DistinctFunctionalTest.php @@ -3,13 +3,16 @@ namespace MongoDB\Tests\Operation; use MongoDB\Driver\BulkWrite; +use MongoDB\Operation\CreateIndexes; use MongoDB\Operation\Distinct; +use MongoDB\Operation\InsertMany; use MongoDB\Tests\CommandObserver; use PHPUnit\Framework\Attributes\DataProvider; use stdClass; use function is_scalar; use function json_encode; +use function sort; use function usort; use const JSON_THROW_ON_ERROR; @@ -56,6 +59,47 @@ function (array $event): void { ); } + public function testHintOption(): void + { + $this->skipIfServerVersion('<', '7.1.0', 'hint is not supported'); + + $insertMany = new InsertMany($this->getDatabaseName(), $this->getCollectionName(), [ + ['x' => 1], + ['x' => 2, 'y' => 2], + ['y' => 3], + ]); + $insertMany->execute($this->getPrimaryServer()); + + $createIndexes = new CreateIndexes($this->getDatabaseName(), $this->getCollectionName(), [ + ['key' => ['x' => 1], 'sparse' => true, 'name' => 'sparse_x'], + ['key' => ['y' => 1]], + ]); + $createIndexes->execute($this->getPrimaryServer()); + + $hintsUsingSparseIndex = [ + ['x' => 1], + 'sparse_x', + ]; + + foreach ($hintsUsingSparseIndex as $hint) { + $operation = new Distinct($this->getDatabaseName(), $this->getCollectionName(), 'y', [], ['hint' => $hint]); + $this->assertSame([2], $operation->execute($this->getPrimaryServer())); + } + + $hintsNotUsingSparseIndex = [ + ['_id' => 1], + ['y' => 1], + 'y_1', + ]; + + foreach ($hintsNotUsingSparseIndex as $hint) { + $operation = new Distinct($this->getDatabaseName(), $this->getCollectionName(), 'x', [], ['hint' => $hint]); + $values = $operation->execute($this->getPrimaryServer()); + sort($values); + $this->assertSame([1, 2], $values); + } + } + public function testSessionOption(): void { (new CommandObserver())->observe( diff --git a/tests/Operation/DistinctTest.php b/tests/Operation/DistinctTest.php index ad5012068..c8cffc337 100644 --- a/tests/Operation/DistinctTest.php +++ b/tests/Operation/DistinctTest.php @@ -30,6 +30,7 @@ public static function provideInvalidConstructorOptions() { return self::createOptionDataProvider([ 'collation' => self::getInvalidDocumentValues(), + 'hint' => self::getInvalidHintValues(), 'maxTimeMS' => self::getInvalidIntegerValues(), 'readConcern' => self::getInvalidReadConcernValues(), 'readPreference' => self::getInvalidReadPreferenceValues(), @@ -42,6 +43,7 @@ public function testExplainableCommandDocument(): void { $options = [ 'collation' => ['locale' => 'fr'], + 'hint' => '_id_', 'maxTimeMS' => 100, 'readConcern' => new ReadConcern(ReadConcern::LOCAL), 'comment' => 'explain me', @@ -56,6 +58,7 @@ public function testExplainableCommandDocument(): void 'key' => 'f', 'query' => (object) ['x' => 1], 'collation' => (object) ['locale' => 'fr'], + 'hint' => '_id_', 'comment' => 'explain me', 'maxTimeMS' => 100, 'readConcern' => new ReadConcern(ReadConcern::LOCAL),