diff --git a/composer.json b/composer.json index 2ca78742e..57c26d342 100644 --- a/composer.json +++ b/composer.json @@ -10,7 +10,7 @@ { "name": "Derick Rethans", "email": "github@derickrethans.nl" } ], "require": { - "ext-mongodb": "*" + "ext-mongodb": ">=0.5.1" }, "require-dev": { "fzaninotto/faker": "~1.0" diff --git a/src/Client.php b/src/Client.php index f83f0b87f..572a610f0 100644 --- a/src/Client.php +++ b/src/Client.php @@ -7,7 +7,8 @@ use MongoDB\Driver\Manager; use MongoDB\Driver\ReadPreference; use MongoDB\Driver\WriteConcern; -use ArrayIterator; +use MongoDB\Model\DatabaseInfoIterator; +use MongoDB\Model\DatabaseInfoLegacyIterator; use stdClass; use UnexpectedValueException; @@ -54,7 +55,7 @@ public function dropDatabase($databaseName) * List databases. * * @see http://docs.mongodb.org/manual/reference/command/listDatabases/ - * @return Traversable + * @return DatabaseInfoIterator * @throws UnexpectedValueException if the command result is malformed */ public function listDatabases() @@ -62,24 +63,20 @@ public function listDatabases() $command = new Command(array('listDatabases' => 1)); $cursor = $this->manager->executeCommand('admin', $command); + $cursor->setTypeMap(array('document' => 'array')); $result = current($cursor->toArray()); if ( ! isset($result['databases']) || ! is_array($result['databases'])) { throw new UnexpectedValueException('listDatabases command did not return a "databases" array'); } - $databases = array_map( - function(stdClass $database) { return (array) $database; }, - $result['databases'] - ); - - /* Return a Traversable instead of an array in case listDatabases is + /* Return an Iterator instead of an array in case listDatabases is * eventually changed to return a command cursor, like the collection * and index enumeration commands. This makes the "totalSize" command * field inaccessible, but users can manually invoke the command if they * need that value. */ - return new ArrayIterator($databases); + return new DatabaseInfoLegacyIterator($result['databases']); } /** diff --git a/src/Collection.php b/src/Collection.php index 82bc054fa..8512c02cc 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -2,13 +2,19 @@ namespace MongoDB; +use MongoDB\Driver\BulkWrite; use MongoDB\Driver\Command; use MongoDB\Driver\Cursor; use MongoDB\Driver\Manager; use MongoDB\Driver\Query; use MongoDB\Driver\ReadPreference; -use MongoDB\Driver\BulkWrite; +use MongoDB\Driver\Server; use MongoDB\Driver\WriteConcern; +use MongoDB\Exception\InvalidArgumentException; +use MongoDB\Exception\UnexpectedTypeException; +use MongoDB\Model\IndexInfoIterator; +use MongoDB\Model\IndexInfoIteratorIterator; +use MongoDB\Model\IndexInput; class Collection { @@ -244,34 +250,68 @@ public function count(array $filter = array(), array $options = array()) } /** - * Create a single index in the collection. + * Create a single index for the collection. * * @see http://docs.mongodb.org/manual/reference/command/createIndexes/ * @see http://docs.mongodb.org/manual/reference/method/db.collection.createIndex/ - * @param array|object $keys - * @param array $options + * @see Collection::createIndexes() + * @param array|object $key Document containing fields mapped to values, + * which denote order or an index type + * @param array $options Index options * @return string The name of the created index */ - public function createIndex($keys, array $options = array()) + public function createIndex($key, array $options = array()) { - // TODO + return current($this->createIndexes(array(array('key' => $key) + $options))); } /** - * Create multiple indexes in the collection. + * Create one or more indexes for the collection. + * + * Each element in the $indexes array must have a "key" document, which + * contains fields mapped to an order or type. Other options may follow. + * For example: + * + * $indexes = [ + * // Create a unique index on the "username" field + * [ 'key' => [ 'username' => 1 ], 'unique' => true ], + * // Create a 2dsphere index on the "loc" field with a custom name + * [ 'key' => [ 'loc' => '2dsphere' ], 'name' => 'geo' ], + * ]; * - * TODO: decide if $models should be an array of associative arrays, using - * createIndex()'s parameter names as keys, or tuples, using parameters in - * order (e.g. [keys, options]). + * If the "name" option is unspecified, a name will be generated from the + * "key" document. * * @see http://docs.mongodb.org/manual/reference/command/createIndexes/ * @see http://docs.mongodb.org/manual/reference/method/db.collection.createIndex/ - * @param array $models + * @param array $indexes List of index specifications * @return string[] The names of the created indexes + * @throws InvalidArgumentException if an index specification is invalid */ - public function createIndexes(array $models) + public function createIndexes(array $indexes) { - // TODO + if (empty($indexes)) { + return array(); + } + + foreach ($indexes as $i => $index) { + if ( ! is_array($index)) { + throw new UnexpectedTypeException($index, 'array'); + } + + if ( ! isset($index['ns'])) { + $index['ns'] = $this->ns; + } + + $indexes[$i] = new IndexInput($index); + } + + $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); + $server = $this->manager->selectServer($readPreference); + + return (FeatureDetection::isSupported($server, FeatureDetection::API_CREATEINDEXES_CMD)) + ? $this->createIndexesCommand($server, $indexes) + : $this->createIndexesLegacy($server, $indexes); } /** @@ -354,11 +394,24 @@ public function drop() * @see http://docs.mongodb.org/manual/reference/method/db.collection.dropIndex/ * @param string $indexName * @return Cursor - * @throws InvalidArgumentException if "*" is specified + * @throws InvalidArgumentException if $indexName is an empty string or "*" */ public function dropIndex($indexName) { - // TODO + $indexName = (string) $indexName; + + if ($indexName === '') { + throw new InvalidArgumentException('Index name cannot be empty'); + } + + if ($indexName === '*') { + throw new InvalidArgumentException('dropIndexes() must be used to drop multiple indexes'); + } + + $command = new Command(array('dropIndexes' => $this->collname, 'index' => $indexName)); + $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); + + return $this->manager->executeCommand($this->dbname, $command, $readPreference); } /** @@ -370,7 +423,10 @@ public function dropIndex($indexName) */ public function dropIndexes() { - // TODO + $command = new Command(array('dropIndexes' => $this->collname, 'index' => '*')); + $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); + + return $this->manager->executeCommand($this->dbname, $command, $readPreference); } /** @@ -949,15 +1005,20 @@ public function insertOne(array $document) } /** - * Returns information for all indexes in the collection. + * Returns information for all indexes for the collection. * * @see http://docs.mongodb.org/manual/reference/command/listIndexes/ * @see http://docs.mongodb.org/manual/reference/method/db.collection.getIndexes/ - * @return Cursor + * @return IndexInfoIterator */ public function listIndexes() { - // TODO + $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); + $server = $this->manager->selectServer($readPreference); + + return (FeatureDetection::isSupported($server, FeatureDetection::API_LISTINDEXES_CMD)) + ? $this->listIndexesCommand($server) + : $this->listIndexesLegacy($server); } /** @@ -1136,4 +1197,78 @@ protected function _update($filter, $update, $options) $bulk->update($filter, $update, $options); return $this->manager->executeBulkWrite($this->ns, $bulk, $this->wc); } + + /** + * Create one or more indexes for the collection using the createIndexes + * command. + * + * @param Server $server + * @param IndexInput[] $indexes + * @return string[] The names of the created indexes + */ + private function createIndexesCommand(Server $server, array $indexes) + { + $command = new Command(array( + 'createIndexes' => $this->collname, + 'indexes' => $indexes, + )); + $server->executeCommand($this->dbname, $command); + + return array_map(function(IndexInput $index) { return (string) $index; }, $indexes); + } + + /** + * Create one or more indexes for the collection by inserting into the + * "system.indexes" collection (MongoDB <2.6). + * + * @param Server $server + * @param IndexInput[] $indexes + * @return string[] The names of the created indexes + */ + private function createIndexesLegacy(Server $server, array $indexes) + { + $bulk = new BulkWrite(true); + + foreach ($indexes as $index) { + // TODO: Remove this once PHPC-274 is resolved (see: PHPLIB-87) + $bulk->insert($index->bsonSerialize()); + } + + $server->executeBulkWrite($this->dbname . '.system.indexes', $bulk); + + return array_map(function(IndexInput $index) { return (string) $index; }, $indexes); + } + + /** + * Returns information for all indexes for this collection using the + * listIndexes command. + * + * @see http://docs.mongodb.org/manual/reference/command/listIndexes/ + * @param Server $server + * @return IndexInfoIteratorIterator + */ + private function listIndexesCommand(Server $server) + { + $command = new Command(array('listIndexes' => $this->collname)); + $cursor = $server->executeCommand($this->dbname, $command); + $cursor->setTypeMap(array('document' => 'array')); + + return new IndexInfoIteratorIterator($cursor); + } + + /** + * Returns information for all indexes for this collection by querying the + * "system.indexes" collection (MongoDB <2.8). + * + * @param Server $server + * @return IndexInfoIteratorIterator + */ + private function listIndexesLegacy(Server $server) + { + $query = new Query(array('ns' => $this->ns)); + $cursor = $server->executeQuery($this->dbname . '.system.indexes', $query); + $cursor->setTypeMap(array('document' => 'array')); + + return new IndexInfoIteratorIterator($cursor); + } } diff --git a/src/Database.php b/src/Database.php index 1da5d90db..bbee8dc0b 100644 --- a/src/Database.php +++ b/src/Database.php @@ -101,10 +101,7 @@ public function listCollections(array $options = array()) $readPreference = new ReadPreference(ReadPreference::RP_PRIMARY); $server = $this->manager->selectServer($readPreference); - $serverInfo = $server->getInfo(); - $maxWireVersion = isset($serverInfo['maxWireVersion']) ? $serverInfo['maxWireVersion'] : 0; - - return ($maxWireVersion >= 3) + return (FeatureDetection::isSupported($server, FeatureDetection::API_LISTCOLLECTIONS_CMD)) ? $this->listCollectionsCommand($server, $options) : $this->listCollectionsLegacy($server, $options); } @@ -141,13 +138,14 @@ private function listCollectionsCommand(Server $server, array $options = array() { $command = new Command(array('listCollections' => 1) + $options); $cursor = $server->executeCommand($this->databaseName, $command); + $cursor->setTypeMap(array('document' => 'array')); return new CollectionInfoCommandIterator($cursor); } /** - * Returns information for all collections in this database by querying - * the "system.namespaces" collection (MongoDB <2.8). + * Returns information for all collections in this database by querying the + * "system.namespaces" collection (MongoDB <2.8). * * @param Server $server * @param array $options @@ -177,6 +175,7 @@ private function listCollectionsLegacy(Server $server, array $options = array()) $namespace = $this->databaseName . '.system.namespaces'; $query = new Query($filter); $cursor = $server->executeQuery($namespace, $query); + $cursor->setTypeMap(array('document' => 'array')); return new CollectionInfoLegacyIterator($cursor); } diff --git a/src/Exception/BadMethodCallException.php b/src/Exception/BadMethodCallException.php new file mode 100644 index 000000000..331c3b08d --- /dev/null +++ b/src/Exception/BadMethodCallException.php @@ -0,0 +1,7 @@ +getInfo(); + $maxWireVersion = isset($info['maxWireVersion']) ? (integer) $info['maxWireVersion'] : 0; + $minWireVersion = isset($info['minWireVersion']) ? (integer) $info['minWireVersion'] : 0; + + return ($minWireVersion <= $feature && $maxWireVersion >= $feature); + } +} diff --git a/src/Model/CollectionInfo.php b/src/Model/CollectionInfo.php index 9617250ad..318c5e5c2 100644 --- a/src/Model/CollectionInfo.php +++ b/src/Model/CollectionInfo.php @@ -2,10 +2,20 @@ namespace MongoDB\Model; +/** + * Collection information model class. + * + * This class models the collection information returned by the listCollections + * command or, for legacy servers, queries on the "system.namespaces" + * collection. It provides methods to access options for the collection. + * + * @api + * @see MongoDB\Database::listCollections() + * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-collections.rst + */ class CollectionInfo { - private $name; - private $options; + private $info; /** * Constructor. @@ -14,8 +24,7 @@ class CollectionInfo */ public function __construct(array $info) { - $this->name = (string) $info['name']; - $this->options = isset($info['options']) ? (array) $info['options'] : array(); + $this->info = $info; } /** @@ -25,7 +34,7 @@ public function __construct(array $info) */ public function getName() { - return $this->name; + return (string) $this->info['name']; } /** @@ -35,7 +44,7 @@ public function getName() */ public function getOptions() { - return $this->options; + return isset($this->info['options']) ? (array) $this->info['options'] : array(); } /** @@ -45,7 +54,7 @@ public function getOptions() */ public function isCapped() { - return isset($this->options['capped']) ? (boolean) $this->options['capped'] : false; + return ! empty($this->info['options']['capped']); } /** @@ -55,7 +64,7 @@ public function isCapped() */ public function getCappedMax() { - return isset($this->options['max']) ? (integer) $this->options['max'] : null; + return isset($this->info['options']['max']) ? (integer) $this->info['options']['max'] : null; } /** @@ -65,6 +74,6 @@ public function getCappedMax() */ public function getCappedSize() { - return isset($this->options['size']) ? (integer) $this->options['size'] : null; + return isset($this->info['options']['size']) ? (integer) $this->info['options']['size'] : null; } } diff --git a/src/Model/CollectionInfoCommandIterator.php b/src/Model/CollectionInfoCommandIterator.php index ce0c28737..b0dc98684 100644 --- a/src/Model/CollectionInfoCommandIterator.php +++ b/src/Model/CollectionInfoCommandIterator.php @@ -4,11 +4,24 @@ use IteratorIterator; +/** + * CollectionInfoIterator for listCollections command results. + * + * This iterator may be used to wrap a Cursor returned by the listCollections + * command. + * + * @internal + * @see MongoDB\Database::listCollections() + * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-collections.rst + * @see http://docs.mongodb.org/manual/reference/command/listCollections/ + */ class CollectionInfoCommandIterator extends IteratorIterator implements CollectionInfoIterator { /** * Return the current element as a CollectionInfo instance. * + * @see CollectionInfoIterator::current() + * @see http://php.net/iterator.current * @return CollectionInfo */ public function current() diff --git a/src/Model/CollectionInfoIterator.php b/src/Model/CollectionInfoIterator.php index f9e0b6135..0c904d3ab 100644 --- a/src/Model/CollectionInfoIterator.php +++ b/src/Model/CollectionInfoIterator.php @@ -4,6 +4,14 @@ use Iterator; +/** + * CollectionInfoIterator interface. + * + * This iterator is used for enumerating collections in a database. + * + * @api + * @see MongoDB\Database::listCollections() + */ interface CollectionInfoIterator extends Iterator { /** diff --git a/src/Model/CollectionInfoLegacyIterator.php b/src/Model/CollectionInfoLegacyIterator.php index 4426def61..01a6a33df 100644 --- a/src/Model/CollectionInfoLegacyIterator.php +++ b/src/Model/CollectionInfoLegacyIterator.php @@ -7,6 +7,20 @@ use IteratorIterator; use Traversable; +/** + * CollectionInfoIterator for legacy "system.namespaces" query results. + * + * This iterator may be used to wrap a Cursor returned for queries on the + * "system.namespaces" collection. It includes logic to filter out internal + * collections and modify the collection name to be consistent with results from + * the listCollections command. + * + * @internal + * @see MongoDB\Database::listCollections() + * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-collections.rst + * @see http://docs.mongodb.org/manual/reference/command/listCollections/ + * @see http://docs.mongodb.org/manual/reference/system-collections/ + */ class CollectionInfoLegacyIterator extends FilterIterator implements CollectionInfoIterator { /** @@ -29,6 +43,8 @@ public function __construct(Traversable $iterator) /** * Return the current element as a CollectionInfo instance. * + * @see CollectionInfoIterator::current() + * @see http://php.net/iterator.current * @return CollectionInfo */ public function current() @@ -48,7 +64,7 @@ public function current() /** * Filter out internal or invalid collections. * - * @see http://php.net/manual/en/filteriterator.accept.php + * @see http://php.net/filteriterator.accept * @return boolean */ public function accept() diff --git a/src/Model/DatabaseInfo.php b/src/Model/DatabaseInfo.php new file mode 100644 index 000000000..b304ab63a --- /dev/null +++ b/src/Model/DatabaseInfo.php @@ -0,0 +1,58 @@ +info = $info; + } + + /** + * Return the database name. + * + * @return string + */ + public function getName() + { + return (string) $this->info['name']; + } + + /** + * Return the databases size on disk (in bytes). + * + * @return integer + */ + public function getSizeOnDisk() + { + return (integer) $this->info['sizeOnDisk']; + } + + /** + * Return whether the database is empty. + * + * @return boolean + */ + public function isEmpty() + { + return (boolean) $this->info['empty']; + } +} diff --git a/src/Model/DatabaseInfoIterator.php b/src/Model/DatabaseInfoIterator.php new file mode 100644 index 000000000..e18ef18aa --- /dev/null +++ b/src/Model/DatabaseInfoIterator.php @@ -0,0 +1,23 @@ +databases = $databases; + } + + /** + * Return the current element as a DatabaseInfo instance. + * + * @see DatabaseInfoIterator::current() + * @see http://php.net/iterator.current + * @return DatabaseInfo + */ + public function current() + { + return new DatabaseInfo(current($this->databases)); + } + + /** + * Return the key of the current element. + * + * @see http://php.net/iterator.key + * @return integer + */ + public function key() + { + return key($this->databases); + } + + /** + * Move forward to next element. + * + * @see http://php.net/iterator.next + */ + public function next() + { + next($this->databases); + } + + /** + * Rewind the Iterator to the first element. + * + * @see http://php.net/iterator.rewind + */ + public function rewind() + { + reset($this->databases); + } + + /** + * Checks if current position is valid. + * + * @see http://php.net/iterator.valid + * @return boolean + */ + public function valid() + { + return key($this->databases) !== null; + } +} diff --git a/src/Model/IndexInfo.php b/src/Model/IndexInfo.php new file mode 100644 index 000000000..685298d7c --- /dev/null +++ b/src/Model/IndexInfo.php @@ -0,0 +1,160 @@ +info = $info; + } + + /** + * Return the index key(s). + * + * @return array + */ + public function getKeys() + { + return (array) $this->info['key']; + } + + /** + * Return the index name. + * + * @return string + */ + public function getName() + { + return (string) $this->info['name']; + } + + /** + * Return the index namespace (e.g. "db.collection"). + * + * @return string + */ + public function getNamespace() + { + return (string) $this->info['ns']; + } + + /** + * Return the index version. + * + * @return integer + */ + public function getVersion() + { + return (integer) $this->info['v']; + } + + /** + * Return whether this is a sparse index. + * + * @see http://docs.mongodb.org/manual/core/index-sparse/ + * @return boolean + */ + public function isSparse() + { + return ! empty($this->info['sparse']); + } + + /** + * Return whether this is a TTL index. + * + * @see http://docs.mongodb.org/manual/core/index-ttl/ + * @return boolean + */ + public function isTtl() + { + return array_key_exists('expireAfterSeconds', $this->info); + } + + /** + * Return whether this is a unique index. + * + * @see http://docs.mongodb.org/manual/core/index-unique/ + * @return boolean + */ + public function isUnique() + { + return ! empty($this->info['unique']); + } + + /** + * Check whether a field exists in the index information. + * + * @see http://php.net/arrayaccess.offsetexists + * @param mixed $key + * @return boolean + */ + public function offsetExists($key) + { + return array_key_exists($key, $this->info); + } + + /** + * Return the field's value from the index information. + * + * This method satisfies the Enumerating Indexes specification's requirement + * that index fields be made accessible under their original names. It may + * also be used to access fields that do not have a helper method. + * + * @see http://php.net/arrayaccess.offsetget + * @see https://github.com/mongodb/specifications/blob/master/source/enumerate-indexes.rst#getting-full-index-information + * @param mixed $key + * @return mixed + */ + public function offsetGet($key) + { + return $this->data[$key]; + } + + /** + * Not supported. + * + * @see http://php.net/arrayaccess.offsetset + * @throws BadMethodCallException IndexInfo is immutable + */ + public function offsetSet($key, $value) + { + throw new BadMethodCallException('IndexInfo is immutable'); + } + + /** + * Not supported. + * + * @see http://php.net/arrayaccess.offsetunset + * @throws BadMethodCallException IndexInfo is immutable + */ + public function offsetUnset($key) + { + throw new BadMethodCallException('IndexInfo is immutable'); + } +} diff --git a/src/Model/IndexInfoIterator.php b/src/Model/IndexInfoIterator.php new file mode 100644 index 000000000..e4275bb66 --- /dev/null +++ b/src/Model/IndexInfoIterator.php @@ -0,0 +1,23 @@ +generateName($index['key']); + } + + if ( ! is_string($index['name'])) { + throw new UnexpectedTypeException($index['name'], 'string'); + } + + $this->index = $index; + } + + /** + * Serialize the index information to BSON for index creation. + * + * @see MongoDB\Collection::createIndexes() + * @see http://php.net/bson-serializable.bsonserialize + */ + public function bsonSerialize() + { + return $this->index; + } + + /** + * Return the index name. + * + * @param string + */ + public function __toString() + { + return $this->index['name']; + } + + /** + * Generates an index name from its key specification. + * + * @param array|object $key Document containing fields mapped to values, + * which denote order or an index type + * @return string + */ + private function generateName($key) + { + $name = ''; + + foreach ($key as $field => $type) { + $name .= ($name != '' ? '_' : '') . $field . '_' . $type; + } + + return $name; + } +} diff --git a/tests/ClientFunctionalTest.php b/tests/ClientFunctionalTest.php index 06b67f169..a7f3fe4ee 100644 --- a/tests/ClientFunctionalTest.php +++ b/tests/ClientFunctionalTest.php @@ -3,19 +3,30 @@ namespace MongoDB\Tests; use MongoDB\Client; +use MongoDB\Driver\Command; +use MongoDB\Model\DatabaseInfo; /** * Functional tests for the Client class. */ class ClientFunctionalTest extends FunctionalTestCase { + private $client; + + public function setUp() + { + parent::setUp(); + + $this->client = new Client($this->getUri()); + $this->client->dropDatabase($this->getDatabaseName()); + } + public function testDropDatabase() { $writeResult = $this->manager->executeInsert($this->getNamespace(), array('x' => 1)); $this->assertEquals(1, $writeResult->getInsertedCount()); - $client = new Client($this->getUri()); - $commandResult = $client->dropDatabase($this->getDatabaseName()); + $commandResult = $this->client->dropDatabase($this->getDatabaseName()); $this->assertCommandSucceeded($commandResult); $this->assertCollectionCount($this->getNamespace(), 0); } @@ -25,22 +36,52 @@ public function testListDatabases() $writeResult = $this->manager->executeInsert($this->getNamespace(), array('x' => 1)); $this->assertEquals(1, $writeResult->getInsertedCount()); - $client = new Client($this->getUri()); - $databases = $client->listDatabases(); + $databases = $this->client->listDatabases(); + + $this->assertInstanceOf('MongoDB\Model\DatabaseInfoIterator', $databases); + + foreach ($databases as $database) { + $this->assertInstanceOf('MongoDB\Model\DatabaseInfo', $database); + } + + $that = $this; + $this->assertDatabaseExists($this->getDatabaseName(), function(DatabaseInfo $info) use ($that) { + $that->assertFalse($info->isEmpty()); + $that->assertGreaterThan(0, $info->getSizeOnDisk()); + }); + } + + /** + * Asserts that a database with the given name exists on the server. + * + * An optional $callback may be provided, which should take a DatabaseInfo + * argument as its first and only parameter. If a DatabaseInfo matching + * the given name is found, it will be passed to the callback, which may + * perform additional assertions. + * + * @param callable $callback + */ + private function assertDatabaseExists($databaseName, $callback = null) + { + if ($callback !== null && ! is_callable($callback)) { + throw new InvalidArgumentException('$callback is not a callable'); + } - $this->assertInstanceOf('Traversable', $databases); + $databases = $this->client->listDatabases(); $foundDatabase = null; foreach ($databases as $database) { - if ($database['name'] === $this->getDatabaseName()) { + if ($database->getName() === $databaseName) { $foundDatabase = $database; break; } } - $this->assertNotNull($foundDatabase, 'Found test database in list of databases'); - $this->assertFalse($foundDatabase['empty'], 'Test database is not empty'); - $this->assertGreaterThan(0, $foundDatabase['sizeOnDisk'], 'Test database takes up disk space'); + $this->assertNotNull($foundDatabase, sprintf('Found %s database on the server', $databaseName)); + + if ($callback !== null) { + call_user_func($callback, $foundDatabase); + } } } diff --git a/tests/CollectionFunctionalTest.php b/tests/CollectionFunctionalTest.php index 4d3bc5d20..7c3bd0300 100644 --- a/tests/CollectionFunctionalTest.php +++ b/tests/CollectionFunctionalTest.php @@ -4,6 +4,8 @@ use MongoDB\Collection; use MongoDB\Driver\Manager; +use MongoDB\Model\IndexInfo; +use InvalidArgumentException; class CollectionFunctionalTest extends FunctionalTestCase { @@ -56,4 +58,180 @@ function testInsertAndRetrieve() } $this->assertEquals(0, $n); } + + public function testCreateIndex() + { + $that = $this; + + $this->assertSame('x_1', $this->collection->createIndex(array('x' => 1), array('sparse' => true, 'unique' => true))); + $this->assertIndexExists('x_1', function(IndexInfo $info) use ($that) { + $that->assertTrue($info->isSparse()); + $that->assertTrue($info->isUnique()); + $that->assertFalse($info->isTtl()); + }); + + $this->assertSame('y_-1_z_1', $this->collection->createIndex(array('y' => -1, 'z' => 1))); + $this->assertIndexExists('y_-1_z_1', function(IndexInfo $info) use ($that) { + $that->assertFalse($info->isSparse()); + $that->assertFalse($info->isUnique()); + $that->assertFalse($info->isTtl()); + }); + + $this->assertSame('g_2dsphere_z_1', $this->collection->createIndex(array('g' => '2dsphere', 'z' => 1))); + $this->assertIndexExists('g_2dsphere_z_1', function(IndexInfo $info) use ($that) { + $that->assertFalse($info->isSparse()); + $that->assertFalse($info->isUnique()); + $that->assertFalse($info->isTtl()); + }); + + $this->assertSame('my_ttl', $this->collection->createIndex(array('t' => 1), array('expireAfterSeconds' => 0, 'name' => 'my_ttl'))); + $this->assertIndexExists('my_ttl', function(IndexInfo $info) use ($that) { + $that->assertFalse($info->isSparse()); + $that->assertFalse($info->isUnique()); + $that->assertTrue($info->isTtl()); + }); + } + + public function testCreateIndexes() + { + $that = $this; + + $expectedNames = array('x_1', 'y_-1_z_1', 'g_2dsphere_z_1', 'my_ttl'); + + $indexes = array( + array('key' => array('x' => 1), 'sparse' => true, 'unique' => true), + array('key' => array('y' => -1, 'z' => 1)), + array('key' => array('g' => '2dsphere', 'z' => 1)), + array('key' => array('t' => 1), 'expireAfterSeconds' => 0, 'name' => 'my_ttl'), + ); + + $this->assertSame($expectedNames, $this->collection->createIndexes($indexes)); + + $this->assertIndexExists('x_1', function(IndexInfo $info) use ($that) { + $that->assertTrue($info->isSparse()); + $that->assertTrue($info->isUnique()); + $that->assertFalse($info->isTtl()); + }); + + $this->assertIndexExists('y_-1_z_1', function(IndexInfo $info) use ($that) { + $that->assertFalse($info->isSparse()); + $that->assertFalse($info->isUnique()); + $that->assertFalse($info->isTtl()); + }); + + $this->assertIndexExists('g_2dsphere_z_1', function(IndexInfo $info) use ($that) { + $that->assertFalse($info->isSparse()); + $that->assertFalse($info->isUnique()); + $that->assertFalse($info->isTtl()); + }); + + $this->assertIndexExists('my_ttl', function(IndexInfo $info) use ($that) { + $that->assertFalse($info->isSparse()); + $that->assertFalse($info->isUnique()); + $that->assertTrue($info->isTtl()); + }); + } + + public function testCreateIndexesWithEmptyInputIsNop() + { + $this->assertSame(array(), $this->collection->createIndexes(array())); + } + + public function testDropIndex() + { + $this->assertSame('x_1', $this->collection->createIndex(array('x' => 1))); + $this->assertIndexExists('x_1'); + $this->assertCommandSucceeded($this->collection->dropIndex('x_1')); + + foreach ($this->collection->listIndexes() as $index) { + if ($index->getName() === 'x_1') { + $this->fail('The "x_1" index should have been deleted'); + } + } + } + + /** + * @expectedException MongoDB\Exception\InvalidArgumentException + */ + public function testDropIndexShouldNotAllowEmptyIndexName() + { + $this->assertSame('x_1', $this->collection->createIndex(array('x' => 1))); + $this->assertIndexExists('x_1'); + $this->collection->dropIndex(''); + } + + /** + * @expectedException MongoDB\Exception\InvalidArgumentException + */ + public function testDropIndexShouldNotAllowWildcardCharacter() + { + $this->assertSame('x_1', $this->collection->createIndex(array('x' => 1))); + $this->assertIndexExists('x_1'); + $this->collection->dropIndex('*'); + } + + public function testDropIndexes() + { + $this->assertSame('x_1', $this->collection->createIndex(array('x' => 1))); + $this->assertSame('y_1', $this->collection->createIndex(array('y' => 1))); + $this->assertIndexExists('x_1'); + $this->assertIndexExists('y_1'); + $this->assertCommandSucceeded($this->collection->dropIndexes()); + + foreach ($this->collection->listIndexes() as $index) { + if ($index->getName() === 'x_1') { + $this->fail('The "x_1" index should have been deleted'); + } + + if ($index->getName() === 'y_1') { + $this->fail('The "y_1" index should have been deleted'); + } + } + } + + public function testListIndexes() + { + $this->assertSame('x_1', $this->collection->createIndex(array('x' => 1))); + + $indexes = $this->collection->listIndexes(); + $this->assertInstanceOf('MongoDB\Model\IndexInfoIterator', $indexes); + + foreach ($indexes as $index) { + $this->assertInstanceOf('MongoDB\Model\IndexInfo', $index); + } + } + + /** + * Asserts that an index with the given name exists for the collection. + * + * An optional $callback may be provided, which should take an IndexInfo + * argument as its first and only parameter. If an IndexInfo matching the + * given name is found, it will be passed to the callback, which may perform + * additional assertions. + * + * @param callable $callback + */ + private function assertIndexExists($indexName, $callback = null) + { + if ($callback !== null && ! is_callable($callback)) { + throw new InvalidArgumentException('$callback is not a callable'); + } + + $indexes = $this->collection->listIndexes(); + + $foundIndex = null; + + foreach ($indexes as $index) { + if ($index->getName() === $indexName) { + $foundIndex = $index; + break; + } + } + + $this->assertNotNull($foundIndex, sprintf('Found %s index for the collection', $indexName)); + + if ($callback !== null) { + call_user_func($callback, $foundIndex); + } + } }