From 4edfa163dd5e39806ed8d845df968be03484ffc0 Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Wed, 25 Sep 2024 10:08:34 +0200 Subject: [PATCH] PHPLIB-1513: Remove the mapReduce helper --- UPGRADE-2.0.md | 5 +- psalm-baseline.xml | 17 - src/Collection.php | 48 --- src/MapReduceResult.php | 103 ----- src/Operation/MapReduce.php | 387 ------------------ src/functions.php | 18 - tests/Collection/CollectionFunctionalTest.php | 47 --- tests/FunctionsTest.php | 15 - tests/Operation/MapReduceFunctionalTest.php | 314 -------------- tests/Operation/MapReduceTest.php | 94 ----- tests/SpecTests/Operation.php | 14 +- tests/UnifiedSpecTests/Operation.php | 16 +- 12 files changed, 9 insertions(+), 1069 deletions(-) delete mode 100644 src/MapReduceResult.php delete mode 100644 src/Operation/MapReduce.php delete mode 100644 tests/Operation/MapReduceFunctionalTest.php delete mode 100644 tests/Operation/MapReduceTest.php diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md index 69c55aa9b..e73894c43 100644 --- a/UPGRADE-2.0.md +++ b/UPGRADE-2.0.md @@ -10,14 +10,17 @@ UPGRADE FROM 1.x to 2.0 `MongoDB\Model\IndexInfo` class. * The `maxScan`, `modifiers`, `oplogReplay`, and `snapshot` options for `find` and `findOne` operations have been removed. + * The `MongoDB\Collection::mapReduce` method has been removed. Use aggregation + pipeline instead. * The following classes and interfaces have been removed without replacement: - * `MongoDB\Operation\Executable` + * `MongoDB\MapReduceResult` * `MongoDB\Model\CollectionInfoCommandIterator` * `MongoDB\Model\CollectionInfoIterator` * `MongoDB\Model\DatabaseInfoIterator` * `MongoDB\Model\DatabaseInfoLegacyIterator` * `MongoDB\Model\IndexInfoIterator` * `MongoDB\Model\IndexInfoIteratorIterator` + * `MongoDB\Operation\Executable` GridFS ------ diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 38be103c2..541a5380f 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -744,23 +744,6 @@ - - - result->collection]]> - result->db]]> - options['typeMap']]]> - - - - - - - - - - - - options['typeMap']]]> diff --git a/src/Collection.php b/src/Collection.php index 8e18dfb3d..88606e449 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -20,7 +20,6 @@ use Countable; use Iterator; use MongoDB\BSON\Document; -use MongoDB\BSON\JavascriptInterface; use MongoDB\BSON\PackedArray; use MongoDB\Builder\BuilderEncoder; use MongoDB\Builder\Pipeline; @@ -63,7 +62,6 @@ use MongoDB\Operation\InsertOne; use MongoDB\Operation\ListIndexes; use MongoDB\Operation\ListSearchIndexes; -use MongoDB\Operation\MapReduce; use MongoDB\Operation\RenameCollection; use MongoDB\Operation\ReplaceOne; use MongoDB\Operation\UpdateMany; @@ -77,11 +75,7 @@ use function array_key_exists; use function current; use function is_array; -use function sprintf; use function strlen; -use function trigger_error; - -use const E_USER_DEPRECATED; class Collection { @@ -908,48 +902,6 @@ public function listSearchIndexes(array $options = []): Iterator return $operation->execute($server); } - /** - * Executes a map-reduce aggregation on the collection. - * - * @see MapReduce::__construct() for supported options - * @see https://mongodb.com/docs/manual/reference/command/mapReduce/ - * @param JavascriptInterface $map Map function - * @param JavascriptInterface $reduce Reduce function - * @param string|array|object $out Output specification - * @param array $options Command options - * @throws UnsupportedException if options are not supported by the selected server - * @throws InvalidArgumentException for parameter/option parsing errors - * @throws DriverRuntimeException for other driver errors (e.g. connection errors) - * @throws UnexpectedValueException if the command response was malformed - */ - public function mapReduce(JavascriptInterface $map, JavascriptInterface $reduce, string|array|object $out, array $options = []): MapReduceResult - { - @trigger_error(sprintf('The %s method is deprecated and will be removed in a version 2.0.', __METHOD__), E_USER_DEPRECATED); - - $hasOutputCollection = ! is_mapreduce_output_inline($out); - - // Check if the out option is inline because we will want to coerce a primary read preference if not - if ($hasOutputCollection) { - $options['readPreference'] = new ReadPreference(ReadPreference::PRIMARY); - } else { - $options = $this->inheritReadPreference($options); - } - - /* A "majority" read concern is not compatible with inline output, so - * avoid providing the Collection's read concern if it would conflict. - */ - if (! $hasOutputCollection || $this->readConcern->getLevel() !== ReadConcern::MAJORITY) { - $options = $this->inheritReadConcern($options); - } - - $options = $this->inheritWriteOptions($options); - $options = $this->inheritTypeMap($options); - - $operation = new MapReduce($this->databaseName, $this->collectionName, $map, $reduce, $out, $options); - - return $operation->execute(select_server_for_write($this->manager, $options)); - } - /** * Renames the collection. * diff --git a/src/MapReduceResult.php b/src/MapReduceResult.php deleted file mode 100644 index dbe0e1dda..000000000 --- a/src/MapReduceResult.php +++ /dev/null @@ -1,103 +0,0 @@ - - * @psalm-type MapReduceCallable = callable(): Traversable - */ -class MapReduceResult implements IteratorAggregate -{ - /** - * @var callable - * @psalm-var MapReduceCallable - */ - private $getIterator; - - private int $executionTimeMS; - - private array $counts; - - private array $timing; - - /** - * Returns various count statistics from the mapReduce command. - */ - public function getCounts(): array - { - return $this->counts; - } - - /** - * Return the command execution time in milliseconds. - */ - public function getExecutionTimeMS(): int - { - return $this->executionTimeMS; - } - - /** - * Return the mapReduce results as a Traversable. - * - * @see https://php.net/iteratoraggregate.getiterator - * @return Traversable - */ - public function getIterator(): Traversable - { - return call_user_func($this->getIterator); - } - - /** - * Returns various timing statistics from the mapReduce command. - * - * Note: timing statistics are only available if the mapReduce command's - * "verbose" option was true; otherwise, an empty array will be returned. - */ - public function getTiming(): array - { - return $this->timing; - } - - /** - * @internal - * @param callable $getIterator Callback that returns a Traversable for mapReduce results - * @param stdClass $result Result document from the mapReduce command - * @psalm-param MapReduceCallable $getIterator - */ - public function __construct(callable $getIterator, stdClass $result) - { - $this->getIterator = $getIterator; - $this->executionTimeMS = isset($result->timeMillis) ? (integer) $result->timeMillis : 0; - $this->counts = isset($result->counts) ? (array) $result->counts : []; - $this->timing = isset($result->timing) ? (array) $result->timing : []; - } -} diff --git a/src/Operation/MapReduce.php b/src/Operation/MapReduce.php deleted file mode 100644 index f0ddca872..000000000 --- a/src/Operation/MapReduce.php +++ /dev/null @@ -1,387 +0,0 @@ -options['bypassDocumentValidation']) && ! is_bool($this->options['bypassDocumentValidation'])) { - throw InvalidArgumentException::invalidType('"bypassDocumentValidation" option', $this->options['bypassDocumentValidation'], 'boolean'); - } - - if (isset($this->options['collation']) && ! is_document($this->options['collation'])) { - throw InvalidArgumentException::expectedDocumentType('"collation" option', $this->options['collation']); - } - - if (isset($this->options['finalize']) && ! $this->options['finalize'] instanceof JavascriptInterface) { - throw InvalidArgumentException::invalidType('"finalize" option', $this->options['finalize'], JavascriptInterface::class); - } - - if (isset($this->options['jsMode']) && ! is_bool($this->options['jsMode'])) { - throw InvalidArgumentException::invalidType('"jsMode" option', $this->options['jsMode'], 'boolean'); - } - - if (isset($this->options['limit']) && ! is_integer($this->options['limit'])) { - throw InvalidArgumentException::invalidType('"limit" option', $this->options['limit'], 'integer'); - } - - if (isset($this->options['maxTimeMS']) && ! is_integer($this->options['maxTimeMS'])) { - throw InvalidArgumentException::invalidType('"maxTimeMS" option', $this->options['maxTimeMS'], 'integer'); - } - - if (isset($this->options['query']) && ! is_document($this->options['query'])) { - throw InvalidArgumentException::expectedDocumentType('"query" option', $this->options['query']); - } - - if (isset($this->options['readConcern']) && ! $this->options['readConcern'] instanceof ReadConcern) { - throw InvalidArgumentException::invalidType('"readConcern" option', $this->options['readConcern'], ReadConcern::class); - } - - if (isset($this->options['readPreference']) && ! $this->options['readPreference'] instanceof ReadPreference) { - throw InvalidArgumentException::invalidType('"readPreference" option', $this->options['readPreference'], ReadPreference::class); - } - - if (isset($this->options['scope']) && ! is_document($this->options['scope'])) { - throw InvalidArgumentException::expectedDocumentType('"scope" option', $this->options['scope']); - } - - if (isset($this->options['session']) && ! $this->options['session'] instanceof Session) { - throw InvalidArgumentException::invalidType('"session" option', $this->options['session'], Session::class); - } - - if (isset($this->options['sort']) && ! is_document($this->options['sort'])) { - throw InvalidArgumentException::expectedDocumentType('"sort" option', $this->options['sort']); - } - - if (isset($this->options['typeMap']) && ! is_array($this->options['typeMap'])) { - throw InvalidArgumentException::invalidType('"typeMap" option', $this->options['typeMap'], 'array'); - } - - if (isset($this->options['verbose']) && ! is_bool($this->options['verbose'])) { - throw InvalidArgumentException::invalidType('"verbose" option', $this->options['verbose'], 'boolean'); - } - - if (isset($this->options['writeConcern']) && ! $this->options['writeConcern'] instanceof WriteConcern) { - throw InvalidArgumentException::invalidType('"writeConcern" option', $this->options['writeConcern'], WriteConcern::class); - } - - if (isset($this->options['bypassDocumentValidation']) && ! $this->options['bypassDocumentValidation']) { - unset($this->options['bypassDocumentValidation']); - } - - if (isset($this->options['readConcern']) && $this->options['readConcern']->isDefault()) { - unset($this->options['readConcern']); - } - - if (isset($this->options['writeConcern']) && $this->options['writeConcern']->isDefault()) { - unset($this->options['writeConcern']); - } - - // Handle deprecation of CodeWScope - if ($map->getScope() !== null) { - @trigger_error('Use of Javascript with scope in "$map" argument for MapReduce is deprecated. Put all scope variables in the "scope" option of the MapReduce operation.', E_USER_DEPRECATED); - } - - if ($reduce->getScope() !== null) { - @trigger_error('Use of Javascript with scope in "$reduce" argument for MapReduce is deprecated. Put all scope variables in the "scope" option of the MapReduce operation.', E_USER_DEPRECATED); - } - - if (isset($this->options['finalize']) && $this->options['finalize']->getScope() !== null) { - @trigger_error('Use of Javascript with scope in "finalize" option for MapReduce is deprecated. Put all scope variables in the "scope" option of the MapReduce operation.', E_USER_DEPRECATED); - } - - $this->checkOutDeprecations($out); - - $this->out = $out; - } - - /** - * Execute the operation. - * - * @throws UnexpectedValueException if the command response was malformed - * @throws UnsupportedException if read concern or write concern is used and unsupported - * @throws DriverRuntimeException for other driver errors (e.g. connection errors) - */ - public function execute(Server $server): MapReduceResult - { - $inTransaction = isset($this->options['session']) && $this->options['session']->isInTransaction(); - if ($inTransaction) { - if (isset($this->options['readConcern'])) { - throw UnsupportedException::readConcernNotSupportedInTransaction(); - } - - if (isset($this->options['writeConcern'])) { - throw UnsupportedException::writeConcernNotSupportedInTransaction(); - } - } - - $hasOutputCollection = ! is_mapreduce_output_inline($this->out); - - $command = $this->createCommand(); - $options = $this->createOptions($hasOutputCollection); - - /* If the mapReduce operation results in a write, use - * executeReadWriteCommand to ensure we're handling the writeConcern - * option. - * In other cases, we use executeCommand as this will prevent the - * mapReduce operation from being retried when retryReads is enabled. - * See https://github.com/mongodb/specifications/blob/master/source/retryable-reads/retryable-reads.rst#unsupported-read-operations. */ - $cursor = $hasOutputCollection - ? $server->executeReadWriteCommand($this->databaseName, $command, $options) - : $server->executeCommand($this->databaseName, $command, $options); - - if (isset($this->options['typeMap']) && ! $hasOutputCollection) { - $cursor->setTypeMap(create_field_path_type_map($this->options['typeMap'], 'results.$')); - } - - $result = current($cursor->toArray()); - assert($result instanceof stdClass); - - $getIterator = $this->createGetIteratorCallable($result, $server); - - return new MapReduceResult($getIterator, $result); - } - - private function checkOutDeprecations(string|array|object $out): void - { - if (is_string($out)) { - return; - } - - $out = document_to_array($out); - - if (isset($out['nonAtomic']) && ! $out['nonAtomic']) { - @trigger_error('Specifying false for "out.nonAtomic" is deprecated.', E_USER_DEPRECATED); - } - - if (isset($out['sharded']) && ! $out['sharded']) { - @trigger_error('Specifying false for "out.sharded" is deprecated.', E_USER_DEPRECATED); - } - } - - /** - * Create the mapReduce command. - */ - private function createCommand(): Command - { - $cmd = [ - 'mapReduce' => $this->collectionName, - 'map' => $this->map, - 'reduce' => $this->reduce, - 'out' => $this->out, - ]; - - foreach (['bypassDocumentValidation', 'comment', 'finalize', 'jsMode', 'limit', 'maxTimeMS', 'verbose'] as $option) { - if (isset($this->options[$option])) { - $cmd[$option] = $this->options[$option]; - } - } - - foreach (['collation', 'query', 'scope', 'sort'] as $option) { - if (isset($this->options[$option])) { - $cmd[$option] = (object) $this->options[$option]; - } - } - - return new Command($cmd); - } - - /** - * Creates a callable for MapReduceResult::getIterator(). - * - * @psalm-return MapReduceCallable - * @throws UnexpectedValueException if the command response was malformed - */ - private function createGetIteratorCallable(stdClass $result, Server $server): callable - { - // Inline results can be wrapped with an ArrayIterator - if (isset($result->results) && is_array($result->results)) { - $results = $result->results; - - return fn () => new ArrayIterator($results); - } - - if (isset($result->result) && (is_string($result->result) || is_object($result->result))) { - $options = isset($this->options['typeMap']) ? ['typeMap' => $this->options['typeMap']] : []; - - $find = is_string($result->result) - ? new Find($this->databaseName, $result->result, [], $options) - : new Find($result->result->db, $result->result->collection, [], $options); - - return fn () => $find->execute($server); - } - - throw new UnexpectedValueException('mapReduce command did not return inline results or an output collection'); - } - - /** - * Create options for executing the command. - * - * @see https://php.net/manual/en/mongodb-driver-server.executereadcommand.php - * @see https://php.net/manual/en/mongodb-driver-server.executereadwritecommand.php - */ - private function createOptions(bool $hasOutputCollection): array - { - $options = []; - - if (isset($this->options['readConcern'])) { - $options['readConcern'] = $this->options['readConcern']; - } - - if (! $hasOutputCollection && isset($this->options['readPreference'])) { - $options['readPreference'] = $this->options['readPreference']; - } - - if (isset($this->options['session'])) { - $options['session'] = $this->options['session']; - } - - if ($hasOutputCollection && isset($this->options['writeConcern'])) { - $options['writeConcern'] = $this->options['writeConcern']; - } - - return $options; - } -} diff --git a/src/functions.php b/src/functions.php index 5d66bd13a..aca396723 100644 --- a/src/functions.php +++ b/src/functions.php @@ -387,24 +387,6 @@ function is_last_pipeline_operator_write(array $pipeline): bool return $key === '$merge' || $key === '$out'; } -/** - * Return whether the "out" option for a mapReduce operation is "inline". - * - * This is used to determine if a mapReduce command requires a primary. - * - * @internal - * @see https://mongodb.com/docs/manual/reference/command/mapReduce/#output-inline - * @param string|array|object $out Output specification - */ -function is_mapreduce_output_inline(string|array|object $out): bool -{ - if (! is_array($out) && ! is_object($out)) { - return false; - } - - return array_key_first(document_to_array($out)) === 'inline'; -} - /** * Return whether the write concern is acknowledged. * diff --git a/tests/Collection/CollectionFunctionalTest.php b/tests/Collection/CollectionFunctionalTest.php index d63140ca4..8a4b318bd 100644 --- a/tests/Collection/CollectionFunctionalTest.php +++ b/tests/Collection/CollectionFunctionalTest.php @@ -3,7 +3,6 @@ namespace MongoDB\Tests\Collection; use Closure; -use MongoDB\BSON\Javascript; use MongoDB\Codec\Encoder; use MongoDB\Collection; use MongoDB\Database; @@ -13,7 +12,6 @@ use MongoDB\Driver\WriteConcern; use MongoDB\Exception\InvalidArgumentException; use MongoDB\Exception\UnsupportedException; -use MongoDB\MapReduceResult; use MongoDB\Operation\Count; use MongoDB\Tests\CommandObserver; use TypeError; @@ -24,7 +22,6 @@ use function json_encode; use function str_contains; use function usort; -use function version_compare; use const JSON_THROW_ON_ERROR; @@ -419,37 +416,6 @@ public function testWithOptionsPassesOptions(): void $this->assertSame(WriteConcern::MAJORITY, $debug['writeConcern']->getW()); } - /** - * @group matrix-testing-exclude-server-4.4-driver-4.0 - * @group matrix-testing-exclude-server-4.4-driver-4.2 - * @group matrix-testing-exclude-server-5.0-driver-4.0 - * @group matrix-testing-exclude-server-5.0-driver-4.2 - */ - public function testMapReduce(): void - { - $this->createFixtures(3); - - $map = new Javascript('function() { emit(1, this.x); }'); - $reduce = new Javascript('function(key, values) { return Array.sum(values); }'); - $out = ['inline' => 1]; - - $result = $this->assertDeprecated( - fn () => $this->collection->mapReduce($map, $reduce, $out), - ); - - $this->assertInstanceOf(MapReduceResult::class, $result); - $expected = [ - [ '_id' => 1.0, 'value' => 66.0 ], - ]; - - $this->assertSameDocuments($expected, $result); - - if (version_compare($this->getServerVersion(), '4.3.0', '<')) { - $this->assertGreaterThanOrEqual(0, $result->getExecutionTimeMS()); - $this->assertNotEmpty($result->getCounts()); - } - } - public static function collectionMethodClosures() { return [ @@ -662,19 +628,6 @@ function($collection, $session, $options = []) { ], */ - /* Disabled, as it's illegal to use mapReduce command in transactions - 'mapReduce' => [ - function($collection, $session, $options = []) { - $collection->mapReduce( - new \MongoDB\BSON\Javascript('function() { emit(this.state, this.pop); }'), - new \MongoDB\BSON\Javascript('function(key, values) { return Array.sum(values) }'), - ['inline' => 1], - ['session' => $session] + $options - ); - }, 'rw' - ], - */ - 'replaceOne' => [ function ($collection, $session, $options = []): void { $collection->replaceOne( diff --git a/tests/FunctionsTest.php b/tests/FunctionsTest.php index cab0b529a..903971e76 100644 --- a/tests/FunctionsTest.php +++ b/tests/FunctionsTest.php @@ -17,7 +17,6 @@ use function MongoDB\is_builder_pipeline; use function MongoDB\is_first_key_operator; use function MongoDB\is_last_pipeline_operator_write; -use function MongoDB\is_mapreduce_output_inline; use function MongoDB\is_pipeline; use function MongoDB\is_write_concern_acknowledged; @@ -160,20 +159,6 @@ public function testIsFirstKeyOperatorArgumentTypeCheck($document): void is_first_key_operator($document); } - /** @dataProvider provideDocumentCasts */ - public function testIsMapReduceOutputInlineWithDocumentValues(callable $cast): void - { - $this->assertTrue(is_mapreduce_output_inline($cast(['inline' => 1]))); - // Note: only the key is significant - $this->assertTrue(is_mapreduce_output_inline($cast(['inline' => 0]))); - $this->assertFalse(is_mapreduce_output_inline($cast(['replace' => 'collectionName']))); - } - - public function testIsMapReduceOutputInlineWithStringValue(): void - { - $this->assertFalse(is_mapreduce_output_inline('collectionName')); - } - /** @dataProvider provideTypeMapValues */ public function testCreateFieldPathTypeMap(array $expected, array $typeMap, $fieldPath = 'field'): void { diff --git a/tests/Operation/MapReduceFunctionalTest.php b/tests/Operation/MapReduceFunctionalTest.php deleted file mode 100644 index 69a39f297..000000000 --- a/tests/Operation/MapReduceFunctionalTest.php +++ /dev/null @@ -1,314 +0,0 @@ -createCollection($this->getDatabaseName(), $this->getCollectionName()); - - (new CommandObserver())->observe( - function (): void { - $operation = new MapReduce( - $this->getDatabaseName(), - $this->getCollectionName(), - new Javascript('function() { emit(this.x, this.y); }'), - new Javascript('function(key, values) { return Array.sum(values); }'), - ['inline' => 1], - ['readConcern' => $this->createDefaultReadConcern()], - ); - - $operation->execute($this->getPrimaryServer()); - }, - function (array $event): void { - $this->assertObjectNotHasProperty('readConcern', $event['started']->getCommand()); - }, - ); - } - - public function testDefaultWriteConcernIsOmitted(): void - { - // Collection must exist for mapReduce command - $this->createCollection($this->getDatabaseName(), $this->getCollectionName()); - $this->dropCollection($this->getDatabaseName(), $this->getCollectionName() . '.output'); - - (new CommandObserver())->observe( - function (): void { - $operation = new MapReduce( - $this->getDatabaseName(), - $this->getCollectionName(), - new Javascript('function() { emit(this.x, this.y); }'), - new Javascript('function(key, values) { return Array.sum(values); }'), - $this->getCollectionName() . '.output', - ['writeConcern' => $this->createDefaultWriteConcern()], - ); - - $operation->execute($this->getPrimaryServer()); - }, - function (array $event): void { - $this->assertObjectNotHasProperty('writeConcern', $event['started']->getCommand()); - }, - ); - } - - public function testFinalize(): void - { - $this->createFixtures(3); - - $map = new Javascript('function() { emit(this.x, this.y); }'); - $reduce = new Javascript('function(key, values) { return Array.sum(values); }'); - $out = ['inline' => 1]; - $finalize = new Javascript('function(key, reducedValue) { return reducedValue; }'); - - $operation = new MapReduce($this->getDatabaseName(), $this->getCollectionName(), $map, $reduce, $out, ['finalize' => $finalize]); - $result = $operation->execute($this->getPrimaryServer()); - - $this->assertNotNull($result); - } - - public function testResult(): void - { - $this->createFixtures(3); - - $map = new Javascript('function() { emit(this.x, this.y); }'); - $reduce = new Javascript('function(key, values) { return Array.sum(values); }'); - $out = ['inline' => 1]; - - $operation = new MapReduce($this->getDatabaseName(), $this->getCollectionName(), $map, $reduce, $out); - $result = $operation->execute($this->getPrimaryServer()); - - $this->assertInstanceOf(MapReduceResult::class, $result); - - if (version_compare($this->getServerVersion(), '4.3.0', '<')) { - $this->assertGreaterThanOrEqual(0, $result->getExecutionTimeMS()); - $this->assertNotEmpty($result->getCounts()); - } - } - - public function testResultIncludesTimingWithVerboseOption(): void - { - $this->skipIfServerVersion('>=', '4.3.0', 'mapReduce statistics are no longer exposed'); - - $this->createFixtures(3); - - $map = new Javascript('function() { emit(this.x, this.y); }'); - $reduce = new Javascript('function(key, values) { return Array.sum(values); }'); - $out = ['inline' => 1]; - - $operation = new MapReduce($this->getDatabaseName(), $this->getCollectionName(), $map, $reduce, $out, ['verbose' => true]); - $result = $operation->execute($this->getPrimaryServer()); - - $this->assertInstanceOf(MapReduceResult::class, $result); - $this->assertGreaterThanOrEqual(0, $result->getExecutionTimeMS()); - $this->assertNotEmpty($result->getCounts()); - $this->assertNotEmpty($result->getTiming()); - } - - public function testResultDoesNotIncludeTimingWithoutVerboseOption(): void - { - $this->skipIfServerVersion('>=', '4.3.0', 'mapReduce statistics are no longer exposed'); - - $this->createFixtures(3); - - $map = new Javascript('function() { emit(this.x, this.y); }'); - $reduce = new Javascript('function(key, values) { return Array.sum(values); }'); - $out = ['inline' => 1]; - - $operation = new MapReduce($this->getDatabaseName(), $this->getCollectionName(), $map, $reduce, $out, ['verbose' => false]); - $result = $operation->execute($this->getPrimaryServer()); - - $this->assertInstanceOf(MapReduceResult::class, $result); - $this->assertGreaterThanOrEqual(0, $result->getExecutionTimeMS()); - $this->assertNotEmpty($result->getCounts()); - $this->assertEmpty($result->getTiming()); - } - - public function testSessionOption(): void - { - $this->createFixtures(3); - - (new CommandObserver())->observe( - function (): void { - $operation = new MapReduce( - $this->getDatabaseName(), - $this->getCollectionName(), - new Javascript('function() { emit(this.x, this.y); }'), - new Javascript('function(key, values) { return Array.sum(values); }'), - ['inline' => 1], - ['session' => $this->createSession()], - ); - - $operation->execute($this->getPrimaryServer()); - }, - function (array $event): void { - $this->assertObjectHasProperty('lsid', $event['started']->getCommand()); - }, - ); - } - - public function testBypassDocumentValidationSetWhenTrue(): void - { - $this->createFixtures(1); - - (new CommandObserver())->observe( - function (): void { - $operation = new MapReduce( - $this->getDatabaseName(), - $this->getCollectionName(), - new Javascript('function() { emit(this.x, this.y); }'), - new Javascript('function(key, values) { return Array.sum(values); }'), - ['inline' => 1], - ['bypassDocumentValidation' => true], - ); - - $operation->execute($this->getPrimaryServer()); - }, - function (array $event): void { - $this->assertObjectHasProperty('bypassDocumentValidation', $event['started']->getCommand()); - $this->assertEquals(true, $event['started']->getCommand()->bypassDocumentValidation); - }, - ); - } - - public function testBypassDocumentValidationUnsetWhenFalse(): void - { - $this->createFixtures(1); - - (new CommandObserver())->observe( - function (): void { - $operation = new MapReduce( - $this->getDatabaseName(), - $this->getCollectionName(), - new Javascript('function() { emit(this.x, this.y); }'), - new Javascript('function(key, values) { return Array.sum(values); }'), - ['inline' => 1], - ['bypassDocumentValidation' => false], - ); - - $operation->execute($this->getPrimaryServer()); - }, - function (array $event): void { - $this->assertObjectNotHasProperty('bypassDocumentValidation', $event['started']->getCommand()); - }, - ); - } - - /** @dataProvider provideTypeMapOptionsAndExpectedDocuments */ - public function testTypeMapOptionWithInlineResults(?array $typeMap, array $expectedDocuments): void - { - $this->createFixtures(3); - - $map = new Javascript('function() { emit(this.x, this.y); }'); - $reduce = new Javascript('function(key, values) { return Array.sum(values); }'); - $out = ['inline' => 1]; - - $operation = new MapReduce($this->getDatabaseName(), $this->getCollectionName(), $map, $reduce, $out, ['typeMap' => $typeMap]); - $results = iterator_to_array($operation->execute($this->getPrimaryServer())); - - $this->assertEquals($this->sortResults($expectedDocuments), $this->sortResults($results)); - } - - public static function provideTypeMapOptionsAndExpectedDocuments() - { - return [ - [ - null, - [ - (object) ['_id' => 1, 'value' => 3], - (object) ['_id' => 2, 'value' => 6], - (object) ['_id' => 3, 'value' => 9], - ], - ], - [ - ['root' => 'array'], - [ - ['_id' => 1, 'value' => 3], - ['_id' => 2, 'value' => 6], - ['_id' => 3, 'value' => 9], - ], - ], - [ - ['root' => 'object'], - [ - (object) ['_id' => 1, 'value' => 3], - (object) ['_id' => 2, 'value' => 6], - (object) ['_id' => 3, 'value' => 9], - ], - ], - ]; - } - - /** @dataProvider provideTypeMapOptionsAndExpectedDocuments */ - public function testTypeMapOptionWithOutputCollection(?array $typeMap, array $expectedDocuments): void - { - $this->createFixtures(3); - - $map = new Javascript('function() { emit(this.x, this.y); }'); - $reduce = new Javascript('function(key, values) { return Array.sum(values); }'); - $out = $this->getCollectionName() . '.output'; - $this->dropCollection($this->getDatabaseName(), $out); - - $operation = new MapReduce($this->getDatabaseName(), $this->getCollectionName(), $map, $reduce, $out, ['typeMap' => $typeMap]); - $results = iterator_to_array($operation->execute($this->getPrimaryServer())); - - $this->assertEquals($this->sortResults($expectedDocuments), $this->sortResults($results)); - - $operation = new Find($this->getDatabaseName(), $out, [], ['typeMap' => $typeMap]); - $cursor = $operation->execute($this->getPrimaryServer()); - - $this->assertEquals($this->sortResults($expectedDocuments), $this->sortResults(iterator_to_array($cursor))); - } - - /** - * Create data fixtures. - */ - private function createFixtures(int $n): void - { - $this->dropCollection($this->getDatabaseName(), $this->getCollectionName()); - $bulkWrite = new BulkWrite(['ordered' => true]); - - for ($i = 1; $i <= $n; $i++) { - $bulkWrite->insert(['x' => $i, 'y' => $i]); - $bulkWrite->insert(['x' => $i, 'y' => $i * 2]); - } - - $result = $this->manager->executeBulkWrite($this->getNamespace(), $bulkWrite); - - $this->assertEquals($n * 2, $result->getInsertedCount()); - } - - private function sortResults(array $results): array - { - $sortFunction = static function ($resultA, $resultB): int { - $idA = is_object($resultA) ? $resultA->_id : $resultA['_id']; - $idB = is_object($resultB) ? $resultB->_id : $resultB['_id']; - - return $idA <=> $idB; - }; - - $sortedResults = $results; - usort($sortedResults, $sortFunction); - - return $sortedResults; - } -} diff --git a/tests/Operation/MapReduceTest.php b/tests/Operation/MapReduceTest.php deleted file mode 100644 index 6db1a9b99..000000000 --- a/tests/Operation/MapReduceTest.php +++ /dev/null @@ -1,94 +0,0 @@ -expectException(TypeError::class); - new MapReduce($this->getDatabaseName(), $this->getCollectionName(), $map, $reduce, $out); - } - - public static function provideInvalidOutValues() - { - return self::wrapValuesForDataProvider([123, 3.14, true]); - } - - /** @dataProvider provideDeprecatedOutValues */ - public function testConstructorOutArgumentDeprecations($out): void - { - $map = new Javascript('function() { emit(this.x, this.y); }'); - $reduce = new Javascript('function(key, values) { return Array.sum(values); }'); - - $this->assertDeprecated(function () use ($map, $reduce, $out): void { - new MapReduce($this->getDatabaseName(), $this->getCollectionName(), $map, $reduce, $out); - }); - } - - public static function provideDeprecatedOutValues(): array - { - return [ - 'nonAtomic:array' => [['nonAtomic' => false]], - 'nonAtomic:object' => [(object) ['nonAtomic' => false]], - 'nonAtomic:Serializable' => [new BSONDocument(['nonAtomic' => false])], - 'nonAtomic:Document' => [Document::fromPHP(['nonAtomic' => false])], - 'sharded:array' => [['sharded' => false]], - 'sharded:object' => [(object) ['sharded' => false]], - 'sharded:Serializable' => [new BSONDocument(['sharded' => false])], - 'sharded:Document' => [Document::fromPHP(['sharded' => false])], - ]; - } - - /** @dataProvider provideInvalidConstructorOptions */ - public function testConstructorOptionTypeChecks(array $options): void - { - $map = new Javascript('function() { emit(this.x, this.y); }'); - $reduce = new Javascript('function(key, values) { return Array.sum(values); }'); - $out = ['inline' => 1]; - - $this->expectException(InvalidArgumentException::class); - new MapReduce($this->getDatabaseName(), $this->getCollectionName(), $map, $reduce, $out, $options); - } - - public static function provideInvalidConstructorOptions() - { - return self::createOptionDataProvider([ - 'bypassDocumentValidation' => self::getInvalidBooleanValues(), - 'collation' => self::getInvalidDocumentValues(), - 'finalize' => self::getInvalidJavascriptValues(), - 'jsMode' => self::getInvalidBooleanValues(), - 'limit' => self::getInvalidIntegerValues(), - 'maxTimeMS' => self::getInvalidIntegerValues(), - 'query' => self::getInvalidDocumentValues(), - 'readConcern' => self::getInvalidReadConcernValues(), - 'readPreference' => self::getInvalidReadPreferenceValues(), - 'scope' => self::getInvalidDocumentValues(), - 'session' => self::getInvalidSessionValues(), - 'sort' => self::getInvalidDocumentValues(), - 'typeMap' => self::getInvalidArrayValues(), - 'verbose' => self::getInvalidBooleanValues(), - 'writeConcern' => self::getInvalidWriteConcernValues(), - ]); - } - - private static function getInvalidJavascriptValues() - { - return [123, 3.14, 'foo', true, [], new stdClass(), new ObjectId()]; - } -} diff --git a/tests/SpecTests/Operation.php b/tests/SpecTests/Operation.php index 3672314cf..d605a52aa 100644 --- a/tests/SpecTests/Operation.php +++ b/tests/SpecTests/Operation.php @@ -12,9 +12,9 @@ use MongoDB\Driver\Server; use MongoDB\Driver\Session; use MongoDB\GridFS\Bucket; -use MongoDB\MapReduceResult; use MongoDB\Operation\FindOneAndReplace; use MongoDB\Operation\FindOneAndUpdate; +use PHPUnit\Framework\Assert; use stdClass; use function array_diff_key; @@ -183,10 +183,6 @@ public function assert(FunctionalTestCase $test, Context $context, bool $bubbleE * is not used (e.g. Command Monitoring spec). */ if ($result instanceof Cursor) { $result = $result->toArray(); - } elseif ($result instanceof MapReduceResult) { - /* For mapReduce operations, we ignore the mapReduce metadata - * and only return the result iterator for evaluation. */ - $result = iterator_to_array($result->getIterator()); } } catch (Exception $e) { $exception = $e; @@ -411,12 +407,8 @@ private function executeForCollection(Collection $collection, Context $context): return $collection->listIndexes($args); case 'mapReduce': - return $collection->mapReduce( - $args['map'], - $args['reduce'], - $args['out'], - array_diff_key($args, ['map' => 1, 'reduce' => 1, 'out' => 1]), - ); + Assert::markTestSkipped('mapReduce is not supported'); + break; case 'watch': return $collection->watch( diff --git a/tests/UnifiedSpecTests/Operation.php b/tests/UnifiedSpecTests/Operation.php index 9957af855..f64803b7b 100644 --- a/tests/UnifiedSpecTests/Operation.php +++ b/tests/UnifiedSpecTests/Operation.php @@ -3,7 +3,6 @@ namespace MongoDB\Tests\UnifiedSpecTests; use Error; -use MongoDB\BSON\Javascript; use MongoDB\ChangeStream; use MongoDB\Client; use MongoDB\Collection; @@ -507,19 +506,8 @@ private function executeForCollection(Collection $collection) ); case 'mapReduce': - assertArrayHasKey('map', $args); - assertArrayHasKey('reduce', $args); - assertArrayHasKey('out', $args); - assertInstanceOf(Javascript::class, $args['map']); - assertInstanceOf(Javascript::class, $args['reduce']); - assertThat($args['out'], logicalOr(new IsType('string'), new IsType('array'), new IsType('object'))); - - return iterator_to_array($collection->mapReduce( - $args['map'], - $args['reduce'], - $args['out'], - array_diff_key($args, ['map' => 1, 'reduce' => 1, 'out' => 1]), - )); + Assert::markTestSkipped('mapReduce operation is not supported'); + break; case 'rename': assertArrayHasKey('to', $args);