diff --git a/docs/includes/apiargs-MongoDBCollection-method-rename-option.yaml b/docs/includes/apiargs-MongoDBCollection-method-rename-option.yaml new file mode 100644 index 000000000..e0d9dbf26 --- /dev/null +++ b/docs/includes/apiargs-MongoDBCollection-method-rename-option.yaml @@ -0,0 +1,27 @@ +source: + file: apiargs-MongoDBCollection-common-option.yaml + ref: typeMap +post: | + This will be used for the returned command result document. +--- +source: + file: apiargs-common-option.yaml + ref: session +--- +source: + file: apiargs-MongoDBCollection-common-option.yaml + ref: writeConcern +post: | + This is not supported for server versions prior to 3.4 and will result in an + exception at execution time if used. +--- +arg_name: option +name: dropTarget +type: boolean +description: | + If ``true``, MongoDB will drop the target before renaming the collection. The + default value is ``false``. +interface: phpmethod +operation: ~ +optional: true +... diff --git a/docs/includes/apiargs-MongoDBCollection-method-rename-param.yaml b/docs/includes/apiargs-MongoDBCollection-method-rename-param.yaml new file mode 100644 index 000000000..459b2789d --- /dev/null +++ b/docs/includes/apiargs-MongoDBCollection-method-rename-param.yaml @@ -0,0 +1,25 @@ +arg_name: param +name: $toCollectionName +type: string +description: | + The new name of the collection. +interface: phpmethod +operation: ~ +optional: false +--- +arg_name: param +name: $toDatabaseName +type: string +description: | + The new database name of the collection. If a new database name is not + specified, the database of the original collection will be used. If the new + name specifies a different database, the command copies the collection + to the new database and drops the source collection. +interface: phpmethod +operation: ~ +optional: true +--- +source: + file: apiargs-common-param.yaml + ref: $options +... diff --git a/docs/includes/apiargs-MongoDBDatabase-method-renameCollection-option.yaml b/docs/includes/apiargs-MongoDBDatabase-method-renameCollection-option.yaml new file mode 100644 index 000000000..973957cf8 --- /dev/null +++ b/docs/includes/apiargs-MongoDBDatabase-method-renameCollection-option.yaml @@ -0,0 +1,27 @@ +source: + file: apiargs-common-option.yaml + ref: session +--- +source: + file: apiargs-MongoDBDatabase-common-option.yaml + ref: typeMap +post: | + This will be used for the returned command result document. +--- +source: + file: apiargs-MongoDBDatabase-common-option.yaml + ref: writeConcern +post: | + This is not supported for server versions prior to 3.4 and will result in an + exception at execution time if used. +--- +arg_name: option +name: dropTarget +type: boolean +description: | + If ``true``, MongoDB will drop the target before renaming the collection. The + default value is ``false``. +interface: phpmethod +operation: ~ +optional: true +... diff --git a/docs/includes/apiargs-MongoDBDatabase-method-renameCollection-param.yaml b/docs/includes/apiargs-MongoDBDatabase-method-renameCollection-param.yaml new file mode 100644 index 000000000..3043f4dd5 --- /dev/null +++ b/docs/includes/apiargs-MongoDBDatabase-method-renameCollection-param.yaml @@ -0,0 +1,34 @@ +arg_name: param +name: $fromCollectionName +type: string +description: | + The name of the collection to rename. +interface: phpmethod +operation: ~ +optional: false +--- +arg_name: param +name: $toCollectionName +type: string +description: | + The new name of the collection. +interface: phpmethod +operation: ~ +optional: false +--- +arg_name: param +name: $toDatabaseName +type: string +description: | + The new database name of the collection. If a new database name is not + specified, the current database will be used. If the new name specifies a + different database, the command copies the collection to the new database + and drops the source collection. +interface: phpmethod +operation: ~ +optional: true +--- +source: + file: apiargs-common-param.yaml + ref: $options +... diff --git a/docs/reference/class/MongoDBCollection.txt b/docs/reference/class/MongoDBCollection.txt index 4ccd2c177..57625ed02 100644 --- a/docs/reference/class/MongoDBCollection.txt +++ b/docs/reference/class/MongoDBCollection.txt @@ -91,6 +91,7 @@ Methods /reference/method/MongoDBCollection-insertOne /reference/method/MongoDBCollection-listIndexes /reference/method/MongoDBCollection-mapReduce + /reference/method/MongoDBCollection-rename /reference/method/MongoDBCollection-replaceOne /reference/method/MongoDBCollection-updateMany /reference/method/MongoDBCollection-updateOne diff --git a/docs/reference/class/MongoDBDatabase.txt b/docs/reference/class/MongoDBDatabase.txt index 464c32c50..7e7cb73d9 100644 --- a/docs/reference/class/MongoDBDatabase.txt +++ b/docs/reference/class/MongoDBDatabase.txt @@ -59,6 +59,7 @@ Methods /reference/method/MongoDBDatabase-listCollectionNames /reference/method/MongoDBDatabase-listCollections /reference/method/MongoDBDatabase-modifyCollection + /reference/method/MongoDBDatabase-renameCollection /reference/method/MongoDBDatabase-selectCollection /reference/method/MongoDBDatabase-selectGridFSBucket /reference/method/MongoDBDatabase-watch diff --git a/docs/reference/method/MongoDBCollection-rename.txt b/docs/reference/method/MongoDBCollection-rename.txt new file mode 100644 index 000000000..23a9290c3 --- /dev/null +++ b/docs/reference/method/MongoDBCollection-rename.txt @@ -0,0 +1,79 @@ +============================= +MongoDB\\Collection::rename() +============================= + +.. versionadded:: 1.10 + +.. default-domain:: mongodb + +.. contents:: On this page + :local: + :backlinks: none + :depth: 1 + :class: singlecol + +Definition +---------- + +.. phpmethod:: MongoDB\\Collection::rename() + + Rename the collection. + + .. code-block:: php + + function rename(string $toCollectionName, ?string $toDatabaseName = null, array $options = []): array|object + + This method has the following parameters: + + .. include:: /includes/apiargs/MongoDBCollection-method-rename-param.rst + + The ``$options`` parameter supports the following options: + + .. include:: /includes/apiargs/MongoDBCollection-method-rename-option.rst + +Return Values +------------- + +An array or object with the result document of the :manual:`renameCollection +` command. The return type will depend on the +``typeMap`` option. + +Errors/Exceptions +----------------- + +.. include:: /includes/extracts/error-unsupportedexception.rst +.. include:: /includes/extracts/error-invalidargumentexception.rst +.. include:: /includes/extracts/error-driver-runtimeexception.rst + +Example +------- + +The following operation renames the ``restaurants`` collection in the ``test`` +database to ``places``: + +.. code-block:: php + + test->restaurants; + + $result = $collection->rename('places'); + + var_dump($result); + +The output would then resemble:: + + object(MongoDB\Model\BSONDocument)#9 (1) { + ["storage":"ArrayObject":private]=> + array(1) { + ["ok"]=> + float(1) + } + } + +See Also +-------- + +- :phpmethod:`MongoDB\\Database::renameCollection()` +- :manual:`renameCollection ` command reference in the MongoDB + manual diff --git a/docs/reference/method/MongoDBDatabase-renameCollection.txt b/docs/reference/method/MongoDBDatabase-renameCollection.txt new file mode 100644 index 000000000..558443195 --- /dev/null +++ b/docs/reference/method/MongoDBDatabase-renameCollection.txt @@ -0,0 +1,79 @@ +===================================== +MongoDB\\Database::renameCollection() +===================================== + +.. versionadded:: 1.10 + +.. default-domain:: mongodb + +.. contents:: On this page + :local: + :backlinks: none + :depth: 1 + :class: singlecol + +Definition +---------- + +.. phpmethod:: MongoDB\\Database::renameCollection() + + Rename a collection within the current database. + + .. code-block:: php + + function renameCollection(string $fromCollectionName, string $toCollectionName, ?string $toDatabaseName = null, array $options = []): array|object + + This method has the following parameters: + + .. include:: /includes/apiargs/MongoDBDatabase-method-renameCollection-param.rst + + The ``$options`` parameter supports the following options: + + .. include:: /includes/apiargs/MongoDBDatabase-method-renameCollection-option.rst + +Return Values +------------- + +An array or object with the result document of the :manual:`renameCollection +` command. The return type will depend on the +``typeMap`` option. + +Errors/Exceptions +----------------- + +.. include:: /includes/extracts/error-unsupportedexception.rst +.. include:: /includes/extracts/error-invalidargumentexception.rst +.. include:: /includes/extracts/error-driver-runtimeexception.rst + +Example +------- + +The following example renames the ``restaurants`` collection in the ``test`` +database to ``places``: + +.. code-block:: php + + test; + + $result = $db->renameCollection('restaurants', 'places'); + + var_dump($result); + +The output would then resemble:: + + object(MongoDB\Model\BSONDocument)#8 (1) { + ["storage":"ArrayObject":private]=> + array(1) { + ["ok"]=> + float(1) + } + } + +See Also +-------- + +- :phpmethod:`MongoDB\\Collection::rename()` +- :manual:`renameCollection ` command reference in the MongoDB + manual diff --git a/src/Collection.php b/src/Collection.php index 85a1a4ecf..6634f37d9 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -53,6 +53,7 @@ use MongoDB\Operation\InsertOne; use MongoDB\Operation\ListIndexes; use MongoDB\Operation\MapReduce; +use MongoDB\Operation\RenameCollection; use MongoDB\Operation\ReplaceOne; use MongoDB\Operation\UpdateMany; use MongoDB\Operation\UpdateOne; @@ -1004,6 +1005,39 @@ public function mapReduce(JavascriptInterface $map, JavascriptInterface $reduce, return $operation->execute($server); } + /** + * Renames the collection. + * + * @see RenameCollection::__construct() for supported options + * @param string $toCollectionName New name of the collection + * @param ?string $toDatabaseName New database name of the collection. Defaults to the original database. + * @param array $options Additional options + * @return array|object Command result document + * @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) + */ + public function rename(string $toCollectionName, ?string $toDatabaseName = null, array $options = []) + { + if (! isset($toDatabaseName)) { + $toDatabaseName = $this->databaseName; + } + + if (! isset($options['typeMap'])) { + $options['typeMap'] = $this->typeMap; + } + + $server = select_server($this->manager, $options); + + if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) { + $options['writeConcern'] = $this->writeConcern; + } + + $operation = new RenameCollection($this->databaseName, $this->collectionName, $toDatabaseName, $toCollectionName, $options); + + return $operation->execute($server); + } + /** * Replaces at most one document matching the filter. * diff --git a/src/Database.php b/src/Database.php index 5d2901280..3bf414ff9 100644 --- a/src/Database.php +++ b/src/Database.php @@ -39,6 +39,7 @@ use MongoDB\Operation\ListCollectionNames; use MongoDB\Operation\ListCollections; use MongoDB\Operation\ModifyCollection; +use MongoDB\Operation\RenameCollection; use MongoDB\Operation\Watch; use Traversable; @@ -470,6 +471,40 @@ public function modifyCollection($collectionName, array $collectionOptions, arra return $operation->execute($server); } + /** + * Rename a collection within this database. + * + * @see RenameCollection::__construct() for supported options + * @param string $fromCollectionName Collection name + * @param string $toCollectionName New name of the collection + * @param ?string $toDatabaseName New database name of the collection. Defaults to the original database. + * @param array $options Additional options + * @return array|object Command result document + * @throws UnsupportedException if options are unsupported on the selected server + * @throws InvalidArgumentException for parameter/option parsing errors + * @throws DriverRuntimeException for other driver errors (e.g. connection errors) + */ + public function renameCollection(string $fromCollectionName, string $toCollectionName, ?string $toDatabaseName = null, array $options = []) + { + if (! isset($toDatabaseName)) { + $toDatabaseName = $this->databaseName; + } + + if (! isset($options['typeMap'])) { + $options['typeMap'] = $this->typeMap; + } + + $server = select_server($this->manager, $options); + + if (! isset($options['writeConcern']) && server_supports_feature($server, self::$wireVersionForWritableCommandWriteConcern) && ! is_in_transaction($options)) { + $options['writeConcern'] = $this->writeConcern; + } + + $operation = new RenameCollection($this->databaseName, $fromCollectionName, $toDatabaseName, $toCollectionName, $options); + + return $operation->execute($server); + } + /** * Select a collection within this database. * diff --git a/src/Operation/RenameCollection.php b/src/Operation/RenameCollection.php new file mode 100644 index 000000000..e7be59c09 --- /dev/null +++ b/src/Operation/RenameCollection.php @@ -0,0 +1,166 @@ +isDefault()) { + unset($options['writeConcern']); + } + + if (isset($options['dropTarget']) && ! is_bool($options['dropTarget'])) { + throw InvalidArgumentException::invalidType('"dropTarget" option', $options['dropTarget'], 'boolean'); + } + + $this->fromNamespace = $fromDatabaseName . '.' . $fromCollectionName; + $this->toNamespace = $toDatabaseName . '.' . $toCollectionName; + $this->options = $options; + } + + /** + * Execute the operation. + * + * @see Executable::execute() + * @param Server $server + * @return array|object Command result document + * @throws UnsupportedException if writeConcern is used and unsupported + * @throws DriverRuntimeException for other driver errors (e.g. connection errors) + */ + public function execute(Server $server) + { + if (isset($this->options['writeConcern']) && ! server_supports_feature($server, self::$wireVersionForWriteConcern)) { + throw UnsupportedException::writeConcernNotSupported(); + } + + $inTransaction = isset($this->options['session']) && $this->options['session']->isInTransaction(); + if ($inTransaction && isset($this->options['writeConcern'])) { + throw UnsupportedException::writeConcernNotSupportedInTransaction(); + } + + $cmd = [ + 'renameCollection' => $this->fromNamespace, + 'to' => $this->toNamespace, + ]; + + if (isset($this->options['dropTarget'])) { + $cmd['dropTarget'] = $this->options['dropTarget']; + } + + $cursor = $server->executeWriteCommand('admin', new Command($cmd), $this->createOptions()); + + if (isset($this->options['typeMap'])) { + $cursor->setTypeMap($this->options['typeMap']); + } + + return current($cursor->toArray()); + } + + /** + * Create options for executing the command. + * + * @see http://php.net/manual/en/mongodb-driver-server.executewritecommand.php + * @return array + */ + private function createOptions() + { + $options = []; + + if (isset($this->options['session'])) { + $options['session'] = $this->options['session']; + } + + if (isset($this->options['writeConcern'])) { + $options['writeConcern'] = $this->options['writeConcern']; + } + + return $options; + } +} diff --git a/tests/Collection/CollectionFunctionalTest.php b/tests/Collection/CollectionFunctionalTest.php index 1c6a995ac..05b71c04b 100644 --- a/tests/Collection/CollectionFunctionalTest.php +++ b/tests/Collection/CollectionFunctionalTest.php @@ -5,6 +5,7 @@ use Closure; use MongoDB\BSON\Javascript; use MongoDB\Collection; +use MongoDB\Database; use MongoDB\Driver\BulkWrite; use MongoDB\Driver\ReadConcern; use MongoDB\Driver\ReadPreference; @@ -332,6 +333,53 @@ public function testFindWithinTransaction(): void } } + public function testRenameToSameDatabase(): void + { + $toCollectionName = $this->getCollectionName() . '.renamed'; + $toCollection = new Collection($this->manager, $this->getDatabaseName(), $toCollectionName); + + $writeResult = $this->collection->insertOne(['_id' => 1]); + $this->assertEquals(1, $writeResult->getInsertedCount()); + + $commandResult = $this->collection->rename($toCollectionName, null, ['dropTarget' => true]); + $this->assertCommandSucceeded($commandResult); + $this->assertCollectionDoesNotExist($this->getCollectionName()); + $this->assertCollectionExists($toCollectionName); + + $this->assertSameDocument(['_id' => 1], $toCollection->findOne()); + $toCollection->drop(); + } + + public function testRenameToDifferentDatabase(): void + { + $toDatabaseName = $this->getDatabaseName() . '_renamed'; + $toDatabase = new Database($this->manager, $toDatabaseName); + + /* When renaming an unsharded collection, mongos requires the source + * and target database to both exist on the primary shard. In practice, this + * means we need to create the target database explicitly. + * See: https://docs.mongodb.com/manual/reference/command/renameCollection/#unsharded-collections + */ + if ($this->isShardedCluster()) { + $toDatabase->foo->insertOne(['_id' => 1]); + } + + $toCollectionName = $this->getCollectionName() . '.renamed'; + $toCollection = new Collection($this->manager, $toDatabaseName, $toCollectionName); + + $writeResult = $this->collection->insertOne(['_id' => 1]); + $this->assertEquals(1, $writeResult->getInsertedCount()); + + $commandResult = $this->collection->rename($toCollectionName, $toDatabaseName); + $this->assertCommandSucceeded($commandResult); + $this->assertCollectionDoesNotExist($this->getCollectionName()); + $this->assertCollectionExists($toCollectionName, $toDatabaseName); + + $this->assertSameDocument(['_id' => 1], $toCollection->findOne()); + + $toDatabase->drop(); + } + public function testWithOptionsInheritsOptions(): void { $collectionOptions = [ diff --git a/tests/Database/CollectionManagementFunctionalTest.php b/tests/Database/CollectionManagementFunctionalTest.php index eac00bcc7..4cb5c6107 100644 --- a/tests/Database/CollectionManagementFunctionalTest.php +++ b/tests/Database/CollectionManagementFunctionalTest.php @@ -2,15 +2,10 @@ namespace MongoDB\Tests\Database; -use InvalidArgumentException; use MongoDB\Driver\BulkWrite; use MongoDB\Model\CollectionInfo; use MongoDB\Model\CollectionInfoIterator; -use function call_user_func; -use function is_callable; -use function sprintf; - /** * Functional tests for collection management methods. */ @@ -23,7 +18,7 @@ public function testCreateCollection(): void $commandResult = $this->database->createCollection($basicCollectionName); $this->assertCommandSucceeded($commandResult); - $this->assertCollectionExists($basicCollectionName, function (CollectionInfo $info) use ($that): void { + $this->assertCollectionExists($basicCollectionName, null, function (CollectionInfo $info) use ($that): void { $that->assertFalse($info->isCapped()); }); @@ -36,7 +31,7 @@ public function testCreateCollection(): void $commandResult = $this->database->createCollection($cappedCollectionName, $cappedCollectionOptions); $this->assertCommandSucceeded($commandResult); - $this->assertCollectionExists($cappedCollectionName, function (CollectionInfo $info) use ($that): void { + $this->assertCollectionExists($cappedCollectionName, null, function (CollectionInfo $info) use ($that): void { $that->assertTrue($info->isCapped()); $that->assertEquals(100, $info->getCappedMax()); $that->assertEquals(1048576, $info->getCappedSize()); @@ -112,38 +107,4 @@ public function testListCollectionNamesWithFilter(): void $this->assertEquals($collectionName, $collection); } } - - /** - * Asserts that a collection with the given name exists in the database. - * - * An optional $callback may be provided, which should take a CollectionInfo - * argument as its first and only parameter. If a CollectionInfo matching - * the given name is found, it will be passed to the callback, which may - * perform additional assertions. - * - * @param callable $callback - */ - private function assertCollectionExists($collectionName, ?callable $callback = null): void - { - if ($callback !== null && ! is_callable($callback)) { - throw new InvalidArgumentException('$callback is not a callable'); - } - - $collections = $this->database->listCollections(); - - $foundCollection = null; - - foreach ($collections as $collection) { - if ($collection->getName() === $collectionName) { - $foundCollection = $collection; - break; - } - } - - $this->assertNotNull($foundCollection, sprintf('Found %s collection in the database', $collectionName)); - - if ($callback !== null) { - call_user_func($callback, $foundCollection); - } - } } diff --git a/tests/Database/DatabaseFunctionalTest.php b/tests/Database/DatabaseFunctionalTest.php index 0a0f1501d..cf64531af 100644 --- a/tests/Database/DatabaseFunctionalTest.php +++ b/tests/Database/DatabaseFunctionalTest.php @@ -2,6 +2,7 @@ namespace MongoDB\Tests\Database; +use MongoDB\Collection; use MongoDB\Database; use MongoDB\Driver\BulkWrite; use MongoDB\Driver\Cursor; @@ -211,6 +212,68 @@ public function testModifyCollection(): void } } + public function testRenameCollectionToSameDatabase(): void + { + $toCollectionName = $this->getCollectionName() . '.renamed'; + $toCollection = new Collection($this->manager, $this->getDatabaseName(), $toCollectionName); + + $bulkWrite = new BulkWrite(); + $bulkWrite->insert(['_id' => 1]); + + $writeResult = $this->manager->executeBulkWrite($this->getNamespace(), $bulkWrite); + $this->assertEquals(1, $writeResult->getInsertedCount()); + + $commandResult = $this->database->renameCollection( + $this->getCollectionName(), + $toCollectionName, + null, + ['dropTarget' => true] + ); + $this->assertCommandSucceeded($commandResult); + $this->assertCollectionDoesNotExist($this->getCollectionName()); + $this->assertCollectionExists($toCollectionName); + + $this->assertSameDocument(['_id' => 1], $toCollection->findOne()); + $toCollection->drop(); + } + + public function testRenameCollectionToDifferentDatabase(): void + { + $toDatabaseName = $this->getDatabaseName() . '_renamed'; + $toDatabase = new Database($this->manager, $toDatabaseName); + + /* When renaming an unsharded collection, mongos requires the source + * and target database to both exist on the primary shard. In practice, this + * means we need to create the target database explicitly. + * See: https://docs.mongodb.com/manual/reference/command/renameCollection/#unsharded-collections + */ + if ($this->isShardedCluster()) { + $toDatabase->foo->insertOne(['_id' => 1]); + } + + $toCollectionName = $this->getCollectionName() . '.renamed'; + $toCollection = new Collection($this->manager, $toDatabaseName, $toCollectionName); + + $bulkWrite = new BulkWrite(); + $bulkWrite->insert(['_id' => 1]); + + $writeResult = $this->manager->executeBulkWrite($this->getNamespace(), $bulkWrite); + $this->assertEquals(1, $writeResult->getInsertedCount()); + + $commandResult = $this->database->renameCollection( + $this->getCollectionName(), + $toCollectionName, + $toDatabaseName + ); + $this->assertCommandSucceeded($commandResult); + $this->assertCollectionDoesNotExist($this->getCollectionName()); + $this->assertCollectionExists($toCollectionName, $toDatabaseName); + + $this->assertSameDocument(['_id' => 1], $toCollection->findOne()); + + $toDatabase->drop(); + } + public function testSelectCollectionInheritsOptions(): void { $databaseOptions = [ diff --git a/tests/FunctionalTestCase.php b/tests/FunctionalTestCase.php index 44c51d8c6..38877c92b 100644 --- a/tests/FunctionalTestCase.php +++ b/tests/FunctionalTestCase.php @@ -16,16 +16,19 @@ use MongoDB\Operation\CreateCollection; use MongoDB\Operation\DatabaseCommand; use MongoDB\Operation\DropCollection; +use MongoDB\Operation\ListCollections; use stdClass; use UnexpectedValueException; use function array_merge; +use function call_user_func; use function count; use function current; use function explode; use function getenv; use function implode; use function is_array; +use function is_callable; use function is_object; use function is_string; use function key; @@ -147,6 +150,71 @@ protected function assertCollectionCount($namespace, $count): void $this->assertEquals($count, $document['n']); } + /** + * Asserts that a collection with the given name does not exist on the + * server. + * + * $databaseName defaults to TestCase::getDatabaseName() if unspecified. + */ + protected function assertCollectionDoesNotExist(string $collectionName, ?string $databaseName = null): void + { + if (! isset($databaseName)) { + $databaseName = $this->getDatabaseName(); + } + + $operation = new ListCollections($this->getDatabaseName()); + $collections = $operation->execute($this->getPrimaryServer()); + + $foundCollection = null; + + foreach ($collections as $collection) { + if ($collection->getName() === $collectionName) { + $foundCollection = $collection; + break; + } + } + + $this->assertNull($foundCollection, sprintf('Collection %s exists', $collectionName)); + } + + /** + * Asserts that a collection with the given name exists on the server. + * + * $databaseName defaults to TestCase::getDatabaseName() if unspecified. + * An optional $callback may be provided, which should take a CollectionInfo + * argument as its first and only parameter. If a CollectionInfo matching + * the given name is found, it will be passed to the callback, which may + * perform additional assertions. + */ + protected function assertCollectionExists(string $collectionName, ?string $databaseName = null, ?callable $callback = null): void + { + if (! isset($databaseName)) { + $databaseName = $this->getDatabaseName(); + } + + if ($callback !== null && ! is_callable($callback)) { + throw new InvalidArgumentException('$callback is not a callable'); + } + + $operation = new ListCollections($databaseName); + $collections = $operation->execute($this->getPrimaryServer()); + + $foundCollection = null; + + foreach ($collections as $collection) { + if ($collection->getName() === $collectionName) { + $foundCollection = $collection; + break; + } + } + + $this->assertNotNull($foundCollection, sprintf('Found %s collection in the database', $collectionName)); + + if ($callback !== null) { + call_user_func($callback, $foundCollection); + } + } + protected function assertCommandSucceeded($document): void { $document = is_object($document) ? (array) $document : $document; diff --git a/tests/GridFS/BucketFunctionalTest.php b/tests/GridFS/BucketFunctionalTest.php index 6ed7f27c8..79c5f345f 100644 --- a/tests/GridFS/BucketFunctionalTest.php +++ b/tests/GridFS/BucketFunctionalTest.php @@ -13,7 +13,6 @@ use MongoDB\GridFS\Exception\StreamException; use MongoDB\Model\BSONDocument; use MongoDB\Model\IndexInfo; -use MongoDB\Operation\ListCollections; use MongoDB\Operation\ListIndexes; use PHPUnit\Framework\Error\Warning; @@ -773,29 +772,6 @@ public function testDanglingOpenWritableStream(): void $this->assertSame('', $output); } - /** - * Asserts that a collection with the given name does not exist on the - * server. - * - * @param string $collectionName - */ - private function assertCollectionDoesNotExist(string $collectionName): void - { - $operation = new ListCollections($this->getDatabaseName()); - $collections = $operation->execute($this->getPrimaryServer()); - - $foundCollection = null; - - foreach ($collections as $collection) { - if ($collection->getName() === $collectionName) { - $foundCollection = $collection; - break; - } - } - - $this->assertNull($foundCollection, sprintf('Collection %s exists', $collectionName)); - } - /** * Asserts that an index with the given name exists for the collection. * diff --git a/tests/Operation/DropCollectionFunctionalTest.php b/tests/Operation/DropCollectionFunctionalTest.php index 50fcc3ac6..696cfefcf 100644 --- a/tests/Operation/DropCollectionFunctionalTest.php +++ b/tests/Operation/DropCollectionFunctionalTest.php @@ -4,10 +4,8 @@ use MongoDB\Operation\DropCollection; use MongoDB\Operation\InsertOne; -use MongoDB\Operation\ListCollections; use MongoDB\Tests\CommandObserver; -use function sprintf; use function version_compare; class DropCollectionFunctionalTest extends FunctionalTestCase @@ -81,27 +79,4 @@ function (array $event): void { } ); } - - /** - * Asserts that a collection with the given name does not exist on the - * server. - * - * @param string $collectionName - */ - private function assertCollectionDoesNotExist(string $collectionName): void - { - $operation = new ListCollections($this->getDatabaseName()); - $collections = $operation->execute($this->getPrimaryServer()); - - $foundCollection = null; - - foreach ($collections as $collection) { - if ($collection->getName() === $collectionName) { - $foundCollection = $collection; - break; - } - } - - $this->assertNull($foundCollection, sprintf('Collection %s exists', $collectionName)); - } } diff --git a/tests/Operation/RenameCollectionFunctionalTest.php b/tests/Operation/RenameCollectionFunctionalTest.php new file mode 100644 index 000000000..c14969681 --- /dev/null +++ b/tests/Operation/RenameCollectionFunctionalTest.php @@ -0,0 +1,161 @@ +toCollectionName = $this->getCollectionName() . '.renamed'; + $operation = new DropCollection($this->getDatabaseName(), $this->toCollectionName); + $operation->execute($this->getPrimaryServer()); + } + + public function tearDown(): void + { + if ($this->hasFailed()) { + return; + } + + $operation = new DropCollection($this->getDatabaseName(), $this->toCollectionName); + $operation->execute($this->getPrimaryServer()); + + parent::tearDown(); + } + + public function testDefaultWriteConcernIsOmitted(): void + { + (new CommandObserver())->observe( + function (): void { + $server = $this->getPrimaryServer(); + + $insertOne = new InsertOne($this->getDatabaseName(), $this->getCollectionName(), ['x' => 1]); + $writeResult = $insertOne->execute($server); + $this->assertEquals(1, $writeResult->getInsertedCount()); + + $operation = new RenameCollection( + $this->getDatabaseName(), + $this->getCollectionName(), + $this->getDatabaseName(), + $this->toCollectionName, + ['writeConcern' => $this->createDefaultWriteConcern()] + ); + + $operation->execute($server); + }, + function (array $event): void { + $this->assertObjectNotHasAttribute('writeConcern', $event['started']->getCommand()); + } + ); + } + + public function testRenameCollectionToNonexistentTarget(): void + { + $server = $this->getPrimaryServer(); + + $insertOne = new InsertOne($this->getDatabaseName(), $this->getCollectionName(), ['_id' => 1]); + $writeResult = $insertOne->execute($server); + $this->assertEquals(1, $writeResult->getInsertedCount()); + + $operation = new RenameCollection( + $this->getDatabaseName(), + $this->getCollectionName(), + $this->getDatabaseName(), + $this->toCollectionName + ); + $commandResult = $operation->execute($server); + + $this->assertCommandSucceeded($commandResult); + $this->assertCollectionDoesNotExist($this->getCollectionName()); + $this->assertCollectionExists($this->toCollectionName); + + $operation = new FindOne($this->getDatabaseName(), $this->toCollectionName, []); + $this->assertSameDocument(['_id' => 1], $operation->execute($server)); + } + + public function testRenameCollectionExistingTarget(): void + { + $server = $this->getPrimaryServer(); + + $insertOne = new InsertOne($this->getDatabaseName(), $this->getCollectionName(), ['_id' => 1]); + $writeResult = $insertOne->execute($server); + $this->assertEquals(1, $writeResult->getInsertedCount()); + + $insertOne = new InsertOne($this->getDatabaseName(), $this->toCollectionName, ['_id' => 1]); + $writeResult = $insertOne->execute($server); + $this->assertEquals(1, $writeResult->getInsertedCount()); + + $this->expectException(CommandException::class); + $this->expectExceptionCode(self::$errorCodeNamespaceExists); + $operation = new RenameCollection( + $this->getDatabaseName(), + $this->getCollectionName(), + $this->getDatabaseName(), + $this->toCollectionName + ); + $operation->execute($server); + } + + public function testRenameNonexistentCollection(): void + { + $this->expectException(CommandException::class); + $this->expectExceptionCode(self::$errorCodeNamespaceNotFound); + $operation = new RenameCollection( + $this->getDatabaseName(), + $this->getCollectionName(), + $this->getDatabaseName(), + $this->toCollectionName + ); + $operation->execute($this->getPrimaryServer()); + } + + public function testSessionOption(): void + { + if (version_compare($this->getServerVersion(), '3.6.0', '<')) { + $this->markTestSkipped('Sessions are not supported'); + } + + (new CommandObserver())->observe( + function (): void { + $server = $this->getPrimaryServer(); + + $insertOne = new InsertOne($this->getDatabaseName(), $this->getCollectionName(), ['x' => 1]); + $writeResult = $insertOne->execute($server); + $this->assertEquals(1, $writeResult->getInsertedCount()); + + $operation = new RenameCollection( + $this->getDatabaseName(), + $this->getCollectionName(), + $this->getDatabaseName(), + $this->toCollectionName, + ['session' => $this->createSession()] + ); + + $operation->execute($server); + }, + function (array $event): void { + $this->assertObjectHasAttribute('lsid', $event['started']->getCommand()); + } + ); + } +} diff --git a/tests/Operation/RenameCollectionTest.php b/tests/Operation/RenameCollectionTest.php new file mode 100644 index 000000000..f25eea78d --- /dev/null +++ b/tests/Operation/RenameCollectionTest.php @@ -0,0 +1,47 @@ +expectException(InvalidArgumentException::class); + new RenameCollection( + $this->getDatabaseName(), + $this->getCollectionName(), + $this->getDatabaseName(), + $this->getCollectionName() . '.renamed', + $options + ); + } + + public function provideInvalidConstructorOptions() + { + $options = []; + + foreach ($this->getInvalidSessionValues() as $value) { + $options[][] = ['session' => $value]; + } + + foreach ($this->getInvalidArrayValues() as $value) { + $options[][] = ['typeMap' => $value]; + } + + foreach ($this->getInvalidWriteConcernValues() as $value) { + $options[][] = ['writeConcern' => $value]; + } + + foreach ($this->getInvalidBooleanValues() as $value) { + $options[][] = ['dropTarget' => $value]; + } + + return $options; + } +}