diff --git a/doctrine-mongo-mapping.xsd b/doctrine-mongo-mapping.xsd index b5d4119b8c..76869f83fd 100644 --- a/doctrine-mongo-mapping.xsd +++ b/doctrine-mongo-mapping.xsd @@ -43,6 +43,7 @@ <xs:element name="lifecycle-callbacks" type="odm:lifecycle-callbacks" minOccurs="0"/> <xs:element name="also-load-methods" type="odm:also-load-methods" minOccurs="0"/> <xs:element name="indexes" type="odm:indexes" minOccurs="0"/> + <xs:element name="shard-key" type="odm:shard-key" minOccurs="0"/> </xs:sequence> <xs:attribute name="db" type="xs:NMTOKEN" /> <xs:attribute name="name" type="xs:string" /> @@ -60,7 +61,7 @@ <xs:complexType name="field"> <xs:sequence> <xs:element name="id-generator-option" type="odm:id-generator-option" minOccurs="0" maxOccurs="unbounded" /> - </xs:sequence> + </xs:sequence> <xs:attribute name="id" type="xs:boolean" default="false" /> <xs:attribute name="name" type="xs:NMTOKEN" /> <xs:attribute name="type" type="xs:NMTOKEN" default="string" /> @@ -82,7 +83,7 @@ <xs:attribute name="sparse" type="xs:boolean" /> <xs:attribute name="unique" type="xs:boolean" /> </xs:complexType> - + <xs:complexType name="id-generator-option"> <xs:attribute name="name" type="xs:NMTOKEN" use="required"/> <xs:attribute name="value" type="xs:string" use="required"/> @@ -275,6 +276,24 @@ </xs:sequence> </xs:complexType> + <xs:complexType name="shard-key"> + <xs:sequence> + <xs:element name="key" type="odm:shard-key-key" minOccurs="1" maxOccurs="unbounded"/> + <xs:element name="option" type="odm:shard-key-option" minOccurs="0" maxOccurs="unbounded"/> + </xs:sequence> + <xs:attribute name="unique" type="xs:boolean"/> + <xs:attribute name="numInitialChunks" type="xs:integer"/> + </xs:complexType> + + <xs:complexType name="shard-key-key"> + <xs:attribute name="name" type="xs:NMTOKEN" use="required"/> + <xs:attribute name="order" type="xs:NMTOKEN" default="asc" /> + </xs:complexType> + + <xs:complexType name="shard-key-option"> + <xs:attribute name="name" type="xs:NMTOKEN" use="required"/> + <xs:attribute name="value" type="xs:NMTOKEN" use="required" /> + </xs:complexType> <xs:complexType name="also-load-method"> <xs:attribute name="method" type="xs:NMTOKEN" use="required"/> diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/DoctrineAnnotations.php b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/DoctrineAnnotations.php index 8f2fe92f86..61d4a24945 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/DoctrineAnnotations.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/DoctrineAnnotations.php @@ -80,3 +80,4 @@ require_once __DIR__ . '/PostLoad.php'; require_once __DIR__ . '/PreFlush.php'; require_once __DIR__ . '/HasLifecycleCallbacks.php'; +require_once __DIR__ . '/ShardKey.php'; diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/Document.php b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/Document.php index cf536475cc..da5929010d 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/Document.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/Document.php @@ -27,5 +27,6 @@ final class Document extends AbstractDocument public $repositoryClass; public $indexes = array(); public $requireIndexes = false; + public $shardKey; public $slaveOkay; } diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/ShardKey.php b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/ShardKey.php new file mode 100644 index 0000000000..f707c975b3 --- /dev/null +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Annotations/ShardKey.php @@ -0,0 +1,30 @@ +<?php +/* + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * This software consists of voluntary contributions made by many individuals + * and is licensed under the MIT license. For more information, see + * <http://www.doctrine-project.org>. + */ + +namespace Doctrine\ODM\MongoDB\Mapping\Annotations; + +use Doctrine\Common\Annotations\Annotation; + +/** @Annotation */ +final class ShardKey extends Annotation +{ + public $keys = array(); + public $unique; + public $numInitialChunks; +} diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php index a850328089..6aa9826da5 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php @@ -20,7 +20,6 @@ namespace Doctrine\ODM\MongoDB\Mapping; use Doctrine\Instantiator\Instantiator; -use Doctrine\ODM\MongoDB\LockException; /** * A <tt>ClassMetadata</tt> instance holds all the object-document mapping metadata @@ -115,6 +114,7 @@ public function __sleep() 'generatorOptions', 'idGenerator', 'indexes', + 'shardKey', ); // The rest of the metadata is only serialized if necessary. diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php index bd80ac4293..e3f570bdcd 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataFactory.php @@ -138,6 +138,7 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS $this->addInheritedFields($class, $parent); $this->addInheritedRelations($class, $parent); $this->addInheritedIndexes($class, $parent); + $this->setInheritedShardKey($class, $parent); $class->setIdentifier($parent->identifier); $class->setVersioned($parent->isVersioned); $class->setVersionField($parent->versionField); @@ -336,4 +337,20 @@ private function addInheritedIndexes(ClassMetadata $subClass, ClassMetadata $par $subClass->addIndex($index['keys'], $index['options']); } } + + /** + * Adds inherited shard key to the subclass mapping. + * + * @param ClassMetadata $subClass + * @param ClassMetadata $parentClass + */ + private function setInheritedShardKey(ClassMetadata $subClass, ClassMetadata $parentClass) + { + if ($parentClass->isSharded()) { + $subClass->setShardKey( + $parentClass->shardKey['keys'], + $parentClass->shardKey['options'] + ); + } + } } diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php index 7aad04e283..8ba727e31a 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadataInfo.php @@ -19,11 +19,10 @@ namespace Doctrine\ODM\MongoDB\Mapping; -use Doctrine\ODM\MongoDB\Utility\CollectionHelper; use Doctrine\ODM\MongoDB\LockException; -use Doctrine\ODM\MongoDB\Mapping\MappingException; use Doctrine\ODM\MongoDB\Proxy\Proxy; use Doctrine\ODM\MongoDB\Types\Type; +use Doctrine\ODM\MongoDB\Utility\CollectionHelper; use InvalidArgumentException; /** @@ -194,6 +193,11 @@ class ClassMetadataInfo implements \Doctrine\Common\Persistence\Mapping\ClassMet */ public $indexes = array(); + /** + * READ-ONLY: Keys and options describing shard key. Only for sharded collections. + */ + public $shardKey; + /** * READ-ONLY: Whether or not queries on this document should require indexes. */ @@ -518,7 +522,7 @@ public function setCustomRepositoryClass($repositoryClassName) if ($this->isEmbeddedDocument) { return; } - + if ($repositoryClassName && strpos($repositoryClassName, '\\') === false && strlen($this->namespace)) { $repositoryClassName = $this->namespace . '\\' . $repositoryClassName; } @@ -798,6 +802,67 @@ public function hasIndexes() return $this->indexes ? true : false; } + /** + * Set shard key for this Document. + * + * @param array $keys Array of document keys. + * @param array $options Array of sharding options. + * + * @throws MappingException + */ + public function setShardKey(array $keys, array $options = array()) + { + if ($this->inheritanceType == self::INHERITANCE_TYPE_SINGLE_COLLECTION && !is_null($this->shardKey)) { + throw MappingException::shardKeyInSingleCollInheritanceSubclass($this->getName()); + } + + if ($this->isEmbeddedDocument) { + throw MappingException::embeddedDocumentCantHaveShardKey($this->getName()); + } + + foreach ($keys as $field) { + if ($this->getTypeOfField($field) == 'increment') { + throw MappingException::noIncrementFieldsAllowedInShardKey($this->getName()); + } + } + + $this->shardKey = array( + 'keys' => array_map(function($value) { + if ($value == 1 || $value == -1) { + return (int) $value; + } + if (is_string($value)) { + $lower = strtolower($value); + if ($lower === 'asc') { + return 1; + } elseif ($lower === 'desc') { + return -1; + } + } + return $value; + }, $keys), + 'options' => $options + ); + } + + /** + * @return array + */ + public function getShardKey() + { + return $this->shardKey; + } + + /** + * Checks whether this document has shard key or not. + * + * @return bool + */ + public function isSharded() + { + return $this->shardKey ? true : false; + } + /** * Sets the change tracking policy used by this class. * @@ -1112,7 +1177,7 @@ public function mapField(array $mapping) $mapping['isCascadeRefresh'] = in_array('refresh', $cascades); $mapping['isCascadeMerge'] = in_array('merge', $cascades); $mapping['isCascadeDetach'] = in_array('detach', $cascades); - + if (isset($mapping['type']) && $mapping['type'] === 'file') { $mapping['file'] = true; } @@ -1160,7 +1225,7 @@ public function mapField(array $mapping) && ! empty($mapping['sort']) && ! CollectionHelper::usesSet($mapping['strategy'])) { throw MappingException::referenceManySortMustNotBeUsedWithNonSetCollectionStrategy($this->name, $mapping['fieldName'], $mapping['strategy']); } - + if ($this->isEmbeddedDocument && $mapping['type'] === 'many' && CollectionHelper::isAtomic($mapping['strategy'])) { throw MappingException::atomicCollectionStrategyNotAllowed($mapping['strategy'], $this->name, $mapping['fieldName']); } @@ -1514,7 +1579,7 @@ public function setFieldValue($document, $field, $value) //so the proxy needs to be loaded first. $document->__load(); } - + $this->reflFields[$field]->setValue($document, $value); } @@ -1531,7 +1596,7 @@ public function getFieldValue($document, $field) if ($document instanceof Proxy && $field !== $this->identifier && ! $document->__isInitialized()) { $document->__load(); } - + return $this->reflFields[$field]->getValue($document); } @@ -1543,8 +1608,6 @@ public function getFieldValue($document, $field) * @return array The field mapping. * * @throws MappingException if the $fieldName is not found in the fieldMappings array - * - * @throws MappingException */ public function getFieldMapping($fieldName) { @@ -1554,6 +1617,26 @@ public function getFieldMapping($fieldName) return $this->fieldMappings[$fieldName]; } + /** + * Gets the field mapping by its DB name. + * E.g. it returns identifier's mapping when called with _id. + * + * @param string $dbFieldName + * + * @return array + * @throws MappingException + */ + public function getFieldMappingByDbFieldName($dbFieldName) + { + foreach ($this->fieldMappings as $mapping) { + if ($mapping['name'] == $dbFieldName) { + return $mapping; + } + } + + throw MappingException::mappingNotFoundByDbName($this->name, $dbFieldName); + } + /** * Check if the field is not null. * @@ -1701,7 +1784,7 @@ public function isIdGeneratorNone() * value to use depending on the column type. * * @param array $mapping The version field mapping array - * + * * @throws LockException */ public function setVersionMapping(array &$mapping) diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php index e946c7bdbe..5f90fa8391 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/AnnotationDriver.php @@ -185,6 +185,11 @@ public function loadMetadataForClass($className, ClassMetadata $class) } } + // Set shard key after all fields to ensure we mapped all its keys + if (isset($classAnnotations['Doctrine\ODM\MongoDB\Mapping\Annotations\ShardKey'])) { + $this->setShardKey($class, $classAnnotations['Doctrine\ODM\MongoDB\Mapping\Annotations\ShardKey']); + } + /** @var $method \ReflectionMethod */ foreach ($reflClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { /* Filter for the declaring class only. Callbacks from parent @@ -240,6 +245,25 @@ private function addIndex(ClassMetadataInfo $class, $index, array $keys = array( $class->addIndex($keys, $options); } + /** + * @param ClassMetadataInfo $class + * @param ODM\ShardKey $shardKey + * + * @throws MappingException + */ + private function setShardKey(ClassMetadataInfo $class, ODM\ShardKey $shardKey) + { + $options = array(); + $allowed = array('unique', 'numInitialChunks'); + foreach ($allowed as $name) { + if (isset($shardKey->$name)) { + $options[$name] = $shardKey->$name; + } + } + + $class->setShardKey($shardKey->keys, $options); + } + /** * Factory method for the Annotation Driver * diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php index 45b78765c5..02ec95f40a 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/XmlDriver.php @@ -116,6 +116,9 @@ public function loadMetadataForClass($className, ClassMetadata $class) $this->addIndex($class, $index); } } + if (isset($xmlRoot->{'shard-key'})) { + $this->setShardKey($class, $xmlRoot->{'shard-key'}[0]); + } if (isset($xmlRoot['require-indexes'])) { $class->setRequireIndexes('true' === (string) $xmlRoot['require-indexes']); } @@ -381,6 +384,41 @@ private function addIndex(ClassMetadataInfo $class, \SimpleXmlElement $xmlIndex) $class->addIndex($keys, $options); } + private function setShardKey(ClassMetadataInfo $class, \SimpleXmlElement $xmlShardkey) + { + $attributes = $xmlShardkey->attributes(); + + $keys = array(); + $options = array(); + foreach ($xmlShardkey->{'key'} as $key) { + $keys[(string) $key['name']] = isset($key['order']) ? (string)$key['order'] : 'asc'; + } + + if (isset($attributes['unique'])) { + $options['unique'] = ('true' === (string) $attributes['unique']); + } + + if (isset($attributes['numInitialChunks'])) { + $options['numInitialChunks'] = (int) $attributes['numInitialChunks']; + } + + if (isset($xmlShardkey->{'option'})) { + foreach ($xmlShardkey->{'option'} as $option) { + $value = (string) $option['value']; + if ($value === 'true') { + $value = true; + } elseif ($value === 'false') { + $value = false; + } elseif (is_numeric($value)) { + $value = preg_match('/^[-]?\d+$/', $value) ? (integer) $value : (float) $value; + } + $options[(string) $option['name']] = $value; + } + } + + $class->setShardKey($keys, $options); + } + /** * {@inheritDoc} */ diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/YamlDriver.php index e9bd476e3b..a4fb2a0721 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/Driver/YamlDriver.php @@ -79,6 +79,9 @@ public function loadMetadataForClass($className, ClassMetadata $class) $class->addIndex($index['keys'], isset($index['options']) ? $index['options'] : array()); } } + if (isset($element['shardKey'])) { + $this->setShardKey($class, $element['shardKey']); + } if (isset($element['inheritanceType'])) { $class->setInheritanceType(constant('Doctrine\ODM\MongoDB\Mapping\ClassMetadata::INHERITANCE_TYPE_' . strtoupper($element['inheritanceType']))); } @@ -335,4 +338,22 @@ protected function loadMappingFile($file) { return Yaml::parse(file_get_contents($file)); } + + private function setShardKey(ClassMetadataInfo $class, array $shardKey) + { + $keys = $shardKey['keys']; + $options = array(); + + if (isset($shardKey['options'])) { + $allowed = array('unique', 'numInitialChunks'); + foreach ($shardKey['options'] as $name => $value) { + if ( ! in_array($name, $allowed, true)) { + continue; + } + $options[$name] = $value; + } + } + + $class->setShardKey($keys, $options); + } } diff --git a/lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php b/lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php index f847acb72c..96d37c6fcc 100644 --- a/lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php +++ b/lib/Doctrine/ODM/MongoDB/Mapping/MappingException.php @@ -57,6 +57,16 @@ public static function mappingNotFound($className, $fieldName) return new self("No mapping found for field '$fieldName' in class '$className'."); } + /** + * @param string $className + * @param string $dbFieldName + * @return MappingException + */ + public static function mappingNotFoundByDbName($className, $dbFieldName) + { + return new self("No mapping found for field by DB name '$dbFieldName' in class '$className'."); + } + /** * @param string $document * @param string $fieldName @@ -255,4 +265,31 @@ public static function referenceManySortMustNotBeUsedWithNonSetCollectionStrateg { return new self("ReferenceMany's sort can not be used with addToSet and pushAll strategies, $strategy used in $className::$fieldName"); } + + /** + * @param $subclassName + * @return MappingException + */ + public static function shardKeyInSingleCollInheritanceSubclass($subclassName) + { + return new self("Shard key overriding in subclass is forbidden for single collection inheritance: $subclassName"); + } + + /** + * @param $className + * @return MappingException + */ + public static function embeddedDocumentCantHaveShardKey($className) + { + return new self("Embedded document can't have shard key: $className"); + } + + /** + * @param string $className + * @return MappingException + */ + public static function noIncrementFieldsAllowedInShardKey($className) + { + return new self("No increment fields allowed in the shard key: $className"); + } } diff --git a/lib/Doctrine/ODM/MongoDB/MongoDBException.php b/lib/Doctrine/ODM/MongoDB/MongoDBException.php index b1ffc8cd46..e8ca840ecb 100644 --- a/lib/Doctrine/ODM/MongoDB/MongoDBException.php +++ b/lib/Doctrine/ODM/MongoDB/MongoDBException.php @@ -144,4 +144,50 @@ public static function invalidValueForType($type, $expected, $got) } return new self(sprintf('%s type requires value of type %s, %s given', $type, $expected, $gotType)); } + + /** + * @param string $field + * @param string $className + * @return MongoDBException + */ + public static function shardKeyFieldCannotBeChanged($field, $className) + { + return new self(sprintf('Shard key field "%s" cannot be changed. Class: %s', $field, $className)); + } + + /** + * @param string $field + * @param string $className + * @return MongoDBException + */ + public static function shardKeyFieldMissing($field, $className) + { + return new self(sprintf('Shard key field "%s" is missing. Class: %s.', $field, $className)); + } + + /** + * @param string $dbName + * @param string $errorMessage + * @return MongoDBException + */ + public static function failedToEnableSharding($dbName, $errorMessage) + { + return new self(sprintf('Failed to enable sharding for database "%s". Error from MongoDB: %s', + $dbName, + $errorMessage + )); + } + + /** + * @param string $className + * @param string $errorMessage + * @return MongoDBException + */ + public static function failedToEnsureDocumentSharding($className, $errorMessage) + { + return new self(sprintf('Failed to ensure sharding for document "%s". Error from MongoDB: %s', + $className, + $errorMessage + )); + } } diff --git a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php index d5aefc65aa..3ebf81be96 100644 --- a/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php +++ b/lib/Doctrine/ODM/MongoDB/Persisters/DocumentPersister.php @@ -23,17 +23,18 @@ use Doctrine\MongoDB\CursorInterface; use Doctrine\ODM\MongoDB\Cursor; use Doctrine\ODM\MongoDB\DocumentManager; -use Doctrine\ODM\MongoDB\Utility\CollectionHelper; use Doctrine\ODM\MongoDB\Hydrator\HydratorFactory; use Doctrine\ODM\MongoDB\LockException; use Doctrine\ODM\MongoDB\LockMode; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; +use Doctrine\ODM\MongoDB\MongoDBException; use Doctrine\ODM\MongoDB\PersistentCollection; use Doctrine\ODM\MongoDB\Proxy\Proxy; use Doctrine\ODM\MongoDB\Query\CriteriaMerger; use Doctrine\ODM\MongoDB\Query\Query; use Doctrine\ODM\MongoDB\Types\Type; use Doctrine\ODM\MongoDB\UnitOfWork; +use Doctrine\ODM\MongoDB\Utility\CollectionHelper; /** * The DocumentPersister is responsible for persisting documents. @@ -276,24 +277,8 @@ public function executeUpserts(array $options = array()) } foreach ($this->queuedUpserts as $oid => $document) { - $data = $this->pb->prepareUpsertData($document); - - // Set the initial version for each upsert - if ($this->class->isVersioned) { - $versionMapping = $this->class->fieldMappings[$this->class->versionField]; - if ($versionMapping['type'] === 'int') { - $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document)); - $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion); - } elseif ($versionMapping['type'] === 'date') { - $nextVersionDateTime = new \DateTime(); - $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp()); - $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime); - } - $data['$set'][$versionMapping['name']] = $nextVersion; - } - try { - $this->executeUpsert($data, $options); + $this->executeUpsert($document, $options); $this->handleCollections($document, $options); unset($this->queuedUpserts[$oid]); } catch (\MongoException $e) { @@ -304,23 +289,42 @@ public function executeUpserts(array $options = array()) } /** - * Executes a single upsert in {@link executeInserts} + * Executes a single upsert in {@link executeUpserts} * - * @param array $data - * @param array $options + * @param object $document + * @param array $options */ - private function executeUpsert(array $data, array $options) + private function executeUpsert($document, array $options) { $options['upsert'] = true; - $criteria = array('_id' => $data['$set']['_id']); - unset($data['$set']['_id']); + $criteria = $this->getQueryForDocument($document); + + $data = $this->pb->prepareUpsertData($document); + + // Set the initial version for each upsert + if ($this->class->isVersioned) { + $versionMapping = $this->class->fieldMappings[$this->class->versionField]; + if ($versionMapping['type'] === 'int') { + $nextVersion = max(1, (int) $this->class->reflFields[$this->class->versionField]->getValue($document)); + $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersion); + } elseif ($versionMapping['type'] === 'date') { + $nextVersionDateTime = new \DateTime(); + $nextVersion = new \MongoDate($nextVersionDateTime->getTimestamp()); + $this->class->reflFields[$this->class->versionField]->setValue($document, $nextVersionDateTime); + } + $data['$set'][$versionMapping['name']] = $nextVersion; + } + + foreach (array_keys($criteria) as $field) { + unset($data['$set'][$field]); + } // Do not send an empty $set modifier if (empty($data['$set'])) { unset($data['$set']); } - /* If there are no modifiers remaining, we're upserting a document with + /* If there are no modifiers remaining, we're upserting a document with * an identifier as its only field. Since a document with the identifier * may already exist, the desired behavior is "insert if not exists" and * NOOP otherwise. MongoDB 2.6+ does not allow empty modifiers, so $set @@ -359,11 +363,18 @@ private function executeUpsert(array $data, array $options) */ public function update($document, array $options = array()) { - $id = $this->uow->getDocumentIdentifier($document); $update = $this->pb->prepareUpdateData($document); - $id = $this->class->getDatabaseIdentifierValue($id); - $query = array('_id' => $id); + $query = $this->getQueryForDocument($document); + + foreach (array_keys($query) as $field) { + unset($update['$set'][$field]); + } + + if (empty($update['$set'])) { + unset($update['$set']); + } + // Include versioning logic to set the new version value in the database // and to ensure the version has not changed since this document object instance @@ -416,8 +427,7 @@ public function update($document, array $options = array()) */ public function delete($document, array $options = array()) { - $id = $this->uow->getDocumentIdentifier($document); - $query = array('_id' => $this->class->getDatabaseIdentifierValue($id)); + $query = $this->getQueryForDocument($document); if ($this->class->isLockable) { $query[$this->class->lockField] = array('$exists' => false); @@ -433,13 +443,15 @@ public function delete($document, array $options = array()) /** * Refreshes a managed document. * - * @param array $id The identifier of the document. + * @param string $id * @param object $document The document to refresh. + * + * @deprecated The first argument is deprecated. */ public function refresh($id, $document) { - $class = $this->dm->getClassMetadata(get_class($document)); - $data = $this->collection->findOne(array('_id' => $id)); + $query = $this->getQueryForDocument($document); + $data = $this->collection->findOne($query); $data = $this->hydratorFactory->hydrate($document, $data); $this->uow->setOriginalDocumentData($document, $data); } @@ -521,6 +533,33 @@ public function loadAll(array $criteria = array(), array $sort = null, $limit = return $cursor; } + /** + * @param object $document + * + * @return array + * @throws MongoDBException + */ + public function getShardKeyQuery($document) + { + if ( ! $this->class->isSharded()) { + return array(); + } + + $shardKey = $this->class->getShardKey(); + $keys = array_keys($shardKey['keys']); + $data = $this->uow->getDocumentActualData($document); + + $shardKeyQueryPart = array(); + foreach ($keys as $key) { + $mapping = $this->class->getFieldMappingByDbFieldName($key); + $this->guardMissingShardKey($document, $key, $data); + $value = Type::getType($mapping['type'])->convertToDatabaseValue($data[$mapping['fieldName']]); + $shardKeyQueryPart[$key] = $value; + } + + return $shardKeyQueryPart; + } + /** * Wraps the supplied base cursor in the corresponding ODM class. * @@ -778,7 +817,7 @@ public function createReferenceManyInverseSideQuery(PersistentCollection $collec private function loadReferenceManyWithRepositoryMethod(PersistentCollection $collection) { $cursor = $this->createReferenceManyWithRepositoryMethodCursor($collection); - $mapping = $collection->getMapping(); + $mapping = $collection->getMapping(); $documents = $cursor->toArray(false); foreach ($documents as $key => $obj) { if (CollectionHelper::isHash($mapping['strategy'])) { @@ -1257,4 +1296,49 @@ private function handleCollections($document, $options) $coll->takeSnapshot(); } } + + /** + * If the document is new, ignore shard key field value, otherwise throw an exception. + * Also, shard key field should be presented in actual document data. + * + * @param object $document + * @param string $shardKeyField + * @param array $actualDocumentData + * + * @throws MongoDBException + */ + private function guardMissingShardKey($document, $shardKeyField, $actualDocumentData) + { + $dcs = $this->uow->getDocumentChangeSet($document); + $isUpdate = $this->uow->isScheduledForUpdate($document); + + $fieldMapping = $this->class->getFieldMappingByDbFieldName($shardKeyField); + $fieldName = $fieldMapping['fieldName']; + + if ($isUpdate && isset($dcs[$fieldName]) && $dcs[$fieldName][0] != $dcs[$fieldName][1]) { + throw MongoDBException::shardKeyFieldCannotBeChanged($shardKeyField, $this->class->getName()); + } + + if (!isset($actualDocumentData[$fieldName])) { + throw MongoDBException::shardKeyFieldMissing($shardKeyField, $this->class->getName()); + } + } + + /** + * Get shard key aware query for single document. + * + * @param object $document + * + * @return array + */ + private function getQueryForDocument($document) + { + $id = $this->uow->getDocumentIdentifier($document); + $id = $this->class->getDatabaseIdentifierValue($id); + + $shardKeyQueryPart = $this->getShardKeyQuery($document); + $query = array_merge(array('_id' => $id), $shardKeyQueryPart); + + return $query; + } } diff --git a/lib/Doctrine/ODM/MongoDB/SchemaManager.php b/lib/Doctrine/ODM/MongoDB/SchemaManager.php index 0685391bf3..6cda2fe4d4 100644 --- a/lib/Doctrine/ODM/MongoDB/SchemaManager.php +++ b/lib/Doctrine/ODM/MongoDB/SchemaManager.php @@ -487,4 +487,98 @@ public function isMongoIndexEquivalentToDocumentIndex($mongoIndex, $documentInde return true; } + + /** + * Ensure collections are sharded for all documents that can be loaded with the + * metadata factory. + * + * @param array $indexOptions Options for `ensureIndex` command. It's performed on an existing collections + * + * @throws MongoDBException + */ + public function ensureSharding(array $indexOptions = array()) + { + foreach ($this->metadataFactory->getAllMetadata() as $class) { + if ($class->isMappedSuperclass || !$class->isSharded()) { + continue; + } + + $this->ensureDocumentSharding($class->name, $indexOptions); + } + } + + /** + * Ensure sharding for collection by document name. + * + * @param string $documentName + * @param array $indexOptions Options for `ensureIndex` command. It's performed on an existing collections. + * + * @throws MongoDBException + */ + public function ensureDocumentSharding($documentName, array $indexOptions = array()) + { + $class = $this->dm->getClassMetadata($documentName); + if ( ! $class->isSharded()) { + return; + } + + $this->enableShardingForDbByDocumentName($documentName); + + do { + $result = $this->runShardCollectionCommand($documentName); + $done = true; + $try = 0; + + if ($result['ok'] != 1 && isset($result['proposedKey'])) { + $this->dm->getDocumentCollection($documentName)->ensureIndex($result['proposedKey'], $indexOptions); + $done = false; + $try++; + } + } while (!$done && $try < 2); + + if ($result['ok'] != 1 && $result['errmsg'] !== 'already sharded') { + throw MongoDBException::failedToEnsureDocumentSharding($documentName, $result['errmsg']); + } + } + + /** + * Enable sharding for database which contains documents with given name. + * + * @param string $documentName + * + * @throws MongoDBException + */ + public function enableShardingForDbByDocumentName($documentName) + { + $dbName = $this->dm->getDocumentDatabase($documentName)->getName(); + $adminDb = $this->dm->getConnection()->selectDatabase('admin'); + $result = $adminDb->command(array('enableSharding' => $dbName)); + + if ($result['ok'] != 1 && $result['errmsg'] !== 'already enabled') { + throw MongoDBException::failedToEnableSharding($dbName, $result['errmsg']); + } + } + + /** + * @param $documentName + * + * @return array + */ + private function runShardCollectionCommand($documentName) + { + $class = $this->dm->getClassMetadata($documentName); + $dbName = $this->dm->getDocumentDatabase($documentName)->getName(); + $shardKey = $class->getShardKey(); + $adminDb = $this->dm->getConnection()->selectDatabase('admin'); + + $result = $adminDb->command( + array( + 'shardCollection' => $dbName . '.' . $class->getCollection(), + 'key' => $shardKey['keys'] + ), + $shardKey['options'] + ); + + return $result; + } } diff --git a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php index 40dc282f81..7d9502149d 100644 --- a/lib/Doctrine/ODM/MongoDB/UnitOfWork.php +++ b/lib/Doctrine/ODM/MongoDB/UnitOfWork.php @@ -175,10 +175,10 @@ class UnitOfWork implements PropertyChangedListener * @var array */ private $collectionUpdates = array(); - + /** * A list of documents related to collections scheduled for update or deletion - * + * * @var array */ private $hasScheduledCollections = array(); @@ -446,7 +446,7 @@ public function commit($document = null, array $options = array()) $this->collectionDeletions = $this->visitedCollections = $this->scheduledForDirtyCheck = - $this->orphanRemovals = + $this->orphanRemovals = $this->hasScheduledCollections = array(); } @@ -2448,7 +2448,7 @@ public function clear($documentName = null) $this->collectionUpdates = $this->collectionDeletions = $this->parentAssociations = - $this->orphanRemovals = + $this->orphanRemovals = $this->hasScheduledCollections = array(); } else { $visited = array(); @@ -2523,11 +2523,11 @@ public function isCollectionScheduledForDeletion(PersistentCollection $coll) { return isset($this->collectionDeletions[spl_object_hash($coll)]); } - + /** * INTERNAL: * Unschedules a collection from being deleted when this UnitOfWork commits. - * + * * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll */ public function unscheduleCollectionDeletion(PersistentCollection $coll) @@ -2561,11 +2561,11 @@ public function scheduleCollectionUpdate(PersistentCollection $coll) $this->scheduleCollectionOwner($coll); } } - + /** * INTERNAL: * Unschedules a collection from being updated when this UnitOfWork commits. - * + * * @param \Doctrine\ODM\MongoDB\PersistentCollection $coll */ public function unscheduleCollectionUpdate(PersistentCollection $coll) @@ -2577,7 +2577,7 @@ public function unscheduleCollectionUpdate(PersistentCollection $coll) unset($this->hasScheduledCollections[spl_object_hash($topmostOwner)][$oid]); } } - + /** * Checks whether a PersistentCollection is scheduled for update. * @@ -2604,22 +2604,22 @@ public function getVisitedCollections($document) ? $this->visitedCollections[$oid] : array(); } - + /** * INTERNAL: * Gets PersistentCollections that are scheduled to update and related to $document - * + * * @param object $document * @return array */ public function getScheduledCollections($document) { $oid = spl_object_hash($document); - return isset($this->hasScheduledCollections[$oid]) + return isset($this->hasScheduledCollections[$oid]) ? $this->hasScheduledCollections[$oid] : array(); } - + /** * Checks whether the document is related to a PersistentCollection * scheduled for update or deletion. @@ -2631,7 +2631,7 @@ public function hasScheduledCollections($document) { return isset($this->hasScheduledCollections[spl_object_hash($document)]); } - + /** * Marks the PersistentCollection's top-level owner as having a relation to * a collection scheduled for update or deletion. @@ -2642,7 +2642,7 @@ public function hasScheduledCollections($document) * If the collection is nested within atomic collection, it is immediately * unscheduled and atomic one is scheduled for update instead. This makes * calculating update data way easier. - * + * * @param PersistentCollection $coll */ private function scheduleCollectionOwner(PersistentCollection $coll) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 06aca8fe1d..52ea03e0aa 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -25,6 +25,7 @@ <groups> <exclude> <group>performance</group> + <group>sharding</group> </exclude> </groups> diff --git a/tests/Doctrine/ODM/MongoDB/Tests/BaseTest.php b/tests/Doctrine/ODM/MongoDB/Tests/BaseTest.php index bb2b5a449f..7951cd5501 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/BaseTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/BaseTest.php @@ -6,17 +6,13 @@ use Doctrine\ODM\MongoDB\DocumentManager; use Doctrine\ODM\MongoDB\Mapping\Driver\AnnotationDriver; use Doctrine\MongoDB\Connection; +use Doctrine\ODM\MongoDB\UnitOfWork; abstract class BaseTest extends \PHPUnit_Framework_TestCase { - /** - * @var \Doctrine\ODM\MongoDB\DocumentManager - */ + /** @var DocumentManager */ protected $dm; - - /** - * @var \Doctrine\ODM\MongoDB\UnitOfWork - */ + /** @var UnitOfWork */ protected $uow; public function setUp() diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/EnsureShardingTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/EnsureShardingTest.php new file mode 100644 index 0000000000..87c5d97689 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/EnsureShardingTest.php @@ -0,0 +1,46 @@ +<?php + +namespace Doctrine\ODM\MongoDB\Tests\Functional; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Tests\BaseTest; + +class EnsureShardingTest extends BaseTest +{ + /** + * @group sharding + */ + public function testEnsureShardingForNewCollection() + { + $class = 'Documents\Sharded\ShardedOne'; + $this->dm->getSchemaManager()->ensureDocumentSharding($class); + + $collection = $this->dm->getDocumentCollection($class); + $indexes = $collection->getIndexInfo(); + $stats = $this->dm->getDocumentDatabase($class)->command(array('collstats' => $collection->getName())); + + $this->assertCount(2, $indexes); + $this->assertSame(array('k' => 1), $indexes[1]['key']); + $this->assertTrue($stats['sharded']); + } + + /** + * @group sharding + */ + public function testEnsureShardingForCollectionWithDocuments() + { + $class = 'Documents\Sharded\ShardedOne'; + $collection = $this->dm->getDocumentCollection($class); + $doc = array('title' => 'hey', 'k' => 'hi'); + $collection->insert($doc); + + $this->dm->getSchemaManager()->ensureDocumentSharding($class); + + $indexes = $collection->getIndexInfo(); + $stats = $this->dm->getDocumentDatabase($class)->command(array('collstats' => $collection->getName())); + + $this->assertCount(2, $indexes); + $this->assertSame(array('k' => 1), $indexes[1]['key']); + $this->assertTrue($stats['sharded']); + } +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReadPreferenceTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReadPreferenceTest.php index 5dc2457580..64db477b85 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReadPreferenceTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReadPreferenceTest.php @@ -41,6 +41,7 @@ public function testHintIsNotSetByDefault() } /** + * @group replication_lag * @dataProvider provideReadPreferenceHints */ public function testHintIsSetOnQuery($readPreference, array $tags = null) @@ -62,6 +63,7 @@ public function testHintIsSetOnQuery($readPreference, array $tags = null) } /** + * @group replication_lag * @dataProvider provideReadPreferenceHints */ public function testHintIsSetOnCursor($readPreference, array $tags = null) @@ -87,6 +89,7 @@ public function testHintIsSetOnCursor($readPreference, array $tags = null) } /** + * @group replication_lag * @dataProvider provideReadPreferenceHints */ public function testHintIsSetOnPersistentCollection($readPreference, array $tags = null) diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReferencePrimerTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReferencePrimerTest.php index 8f1f706b20..606772c687 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReferencePrimerTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ReferencePrimerTest.php @@ -246,6 +246,9 @@ public function testPrimeReferencesIgnoresInitializedProxyObjects() $this->assertEquals(0, $invoked, 'Primer was not invoked when all references were already managed.'); } + /** + * @group replication_lag + */ public function testPrimeReferencesInvokesPrimer() { $group1 = new Group(); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/ShardKeyTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ShardKeyTest.php new file mode 100644 index 0000000000..1d4672ee43 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/ShardKeyTest.php @@ -0,0 +1,145 @@ +<?php + +namespace Doctrine\ODM\MongoDB\Tests\Functional; + +use Doctrine\ODM\MongoDB\DocumentManager; +use Doctrine\ODM\MongoDB\Tests\BaseTest; +use Documents\Sharded\ShardedOne; + +class ShardKeyTest extends BaseTest +{ + public function setUp() + { + parent::setUp(); + + $schemaManager = $this->dm->getSchemaManager(); + $schemaManager->ensureDocumentSharding('Documents\Sharded\ShardedOne'); + } + + /** + * @group sharding + */ + public function testUpdateAfterSave() + { + $queries = array(); + $this->logQueries($queries); + + $o = new ShardedOne(); + $this->dm->persist($o); + $this->dm->flush(); + + /** @var \Documents\Sharded\ShardedOne $o */ + $o = $this->dm->find(get_class($o), $o->id); + $o->title = 'test2'; + $this->dm->flush(); + + $lastQuery = end($queries); + $this->assertTrue($lastQuery['update']); + $this->assertContains('k', array_keys($lastQuery['query'])); + $this->assertEquals($o->key, $lastQuery['query']['k']); + } + + /** + * @group sharding + */ + public function testUpsert() + { + $queries = array(); + $this->logQueries($queries); + + $o = new ShardedOne(); + $o->id = new \MongoId(); + $this->dm->persist($o); + $this->dm->flush(); + + $lastQuery = end($queries); + $this->assertTrue($lastQuery['update']); + $this->assertContains('k', array_keys($lastQuery['query'])); + $this->assertEquals($o->key, $lastQuery['query']['k']); + } + + /** + * @group sharding + */ + public function testRemove() + { + $queries = array(); + $this->logQueries($queries); + + $o = new ShardedOne(); + $this->dm->persist($o); + $this->dm->flush(); + $this->dm->remove($o); + $this->dm->flush(); + + $lastQuery = end($queries); + $this->assertTrue($lastQuery['remove']); + $this->assertContains('k', array_keys($lastQuery['query'])); + $this->assertEquals($o->key, $lastQuery['query']['k']); + } + + /** + * @group sharding + */ + public function testRefresh() + { + $queries = array(); + $this->logQueries($queries); + + $o = new ShardedOne(); + $this->dm->persist($o); + $this->dm->flush(); + $this->dm->refresh($o); + + $lastQuery = end($queries); + $this->assertTrue($lastQuery['findOne']); + $this->assertContains('k', array_keys($lastQuery['query'])); + $this->assertEquals($o->key, $lastQuery['query']['k']); + } + + /** + * @group sharding + * @expectedException \Doctrine\ODM\MongoDB\MongoDBException + */ + public function testUpdateWithShardKeyChangeException() + { + $o = new ShardedOne(); + $this->dm->persist($o); + $this->dm->flush(); + + $o->key = 'testing2'; + $this->dm->flush(); + } + + /** + * @group sharding + * @expectedException \Doctrine\ODM\MongoDB\MongoDBException + */ + public function testUpdateWithUpsertTrue() + { + $o = new ShardedOne(); + $this->dm->persist($o); + $this->dm->flush(); + + $o->key = 'testing2'; + $this->dm->flush(null, array('upsert' => true)); + } + + /** + * Replace DM with the one with enabled query logging + * + * @param $queries + */ + private function logQueries(&$queries) + { + $this->dm->getConnection()->getConfiguration()->setLoggerCallable( + function (array $log) use (&$queries) { + $queries[] = $log; + } + ); + $this->dm = DocumentManager::create( + $this->dm->getConnection(), + $this->dm->getConfiguration() + ); + } +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Functional/SlaveOkayTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Functional/SlaveOkayTest.php index 48402fbad4..2d1dc62e15 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Functional/SlaveOkayTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Functional/SlaveOkayTest.php @@ -20,6 +20,9 @@ public function setUp() $this->dm->clear(); } + /** + * @group replication_lag + */ public function testHintIsNotSetByDefault() { $cursor = $this->dm->getRepository('Documents\User') @@ -36,6 +39,7 @@ public function testHintIsNotSetByDefault() } /** + * @group replication_lag * @dataProvider provideSlaveOkayHints */ public function testHintIsSetOnQuery($slaveOkay) @@ -55,6 +59,7 @@ public function testHintIsSetOnQuery($slaveOkay) } /** + * @group replication_lag * @dataProvider provideSlaveOkayHints */ public function testHintIsSetOnCursor($slaveOkay) @@ -75,6 +80,7 @@ public function testHintIsSetOnCursor($slaveOkay) } /** + * @group replication_lag * @dataProvider provideSlaveOkayHints */ public function testHintIsSetOnPersistentCollection($slaveOkay) diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractMappingDriverTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractMappingDriverTest.php index 21731399dc..a1e3bdd53f 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractMappingDriverTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AbstractMappingDriverTest.php @@ -3,8 +3,6 @@ namespace Doctrine\ODM\MongoDB\Tests\Mapping; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; -use Doctrine\ODM\MongoDB\Mapping\Driver\XmlDriver; -use Doctrine\ODM\MongoDB\Mapping\Driver\YamlDriver; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; abstract class AbstractMappingDriverTest extends \Doctrine\ODM\MongoDB\Tests\BaseTest @@ -320,6 +318,23 @@ public function testIndexes($class) return $class; } + + /** + * @depends testIndexes + * @param ClassMetadata $class + */ + public function testShardKey($class) + { + $shardKey = $class->getShardKey(); + + $this->assertTrue(isset($shardKey['keys']['name']), 'Shard key is not mapped'); + $this->assertEquals(1, $shardKey['keys']['name'], 'Wrong value for shard key'); + + $this->assertTrue(isset($shardKey['options']['unique']), 'Shard key option is not mapped'); + $this->assertTrue($shardKey['options']['unique'], 'Shard key option has wrong value'); + $this->assertTrue(isset($shardKey['options']['numInitialChunks']), 'Shard key option is not mapped'); + $this->assertEquals(4096, $shardKey['options']['numInitialChunks'], 'Shard key option has wrong value'); + } } /** @@ -329,6 +344,7 @@ public function testIndexes($class) * @ODM\DefaultDiscriminatorValue("default") * @ODM\HasLifecycleCallbacks * @ODM\Indexes(@ODM\Index(keys={"createdAt"="asc"},expireAfterSeconds=3600)) + * @ODM\ShardKey(keys={"name"="asc"},unique=true,numInitialChunks=4096) */ class AbstractMappingDriverUser { @@ -519,5 +535,6 @@ public static function loadMetadata(ClassMetadata $metadata) $metadata->addIndex(array('email' => 'desc'), array('unique' => true, 'dropDups' => true)); $metadata->addIndex(array('mysqlProfileId' => 'desc'), array('unique' => true, 'dropDups' => true)); $metadata->addIndex(array('createdAt' => 'asc'), array('expireAfterSeconds' => 3600)); + $metadata->setShardKey(array('name' => 'asc'), array('unique' => true, 'numInitialChunks' => 4096)); } } diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AnnotationDriverTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AnnotationDriverTest.php index 98bf3e04f8..0084012afc 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AnnotationDriverTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/AnnotationDriverTest.php @@ -3,7 +3,6 @@ namespace Doctrine\ODM\MongoDB\Tests\Mapping; use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; -use Doctrine\ODM\MongoDB\Events; use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; class AnnotationDriverTest extends AbstractMappingDriverTest @@ -140,6 +139,15 @@ public function testGetClassNamesReturnsOnlyTheAppropriateClasses() $this->assertNotContains($extraneousClassName, $classes); } + /** + * @expectedException \Doctrine\ODM\MongoDB\Mapping\MappingException + * @expectedExceptionMessage Embedded document can't have shard key + */ + public function testEmbeddedClassCantHaveShardKey() + { + $this->dm->getClassMetadata(__NAMESPACE__ . '\AnnotationDriverEmbeddedWithShardKey'); + } + protected function _loadDriverForCMSDocuments() { $annotationDriver = $this->_loadDriver(); @@ -187,3 +195,13 @@ class AnnotationDriverTestChild extends AnnotationDriverTestParent /** @ODM\String */ public $bar; } + +/** + * @ODM\EmbeddedDocument + * @ODM\ShardKey(keys={"foo"="asc"}) + */ +class AnnotationDriverEmbeddedWithShardKey +{ + /** @ODM\String */ + public $foo; +} diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataInfoTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataInfoTest.php index 77a7f9ece1..0b09b05870 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataInfoTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataInfoTest.php @@ -234,7 +234,7 @@ public function testSimpleReferenceRequiresTargetDocument() 'simple' => true, )); } - + /** * @expectedException \Doctrine\ODM\MongoDB\Mapping\MappingException * @expectedExceptionMessage atomicSet collection strategy can be used only in top level document, used in stdClass::many @@ -328,6 +328,91 @@ public function testReferenceManySortMustNotBeUsedWithNonSetCollectionStrategy() 'sort' => array('foo' => 1) )); } + + public function testSetShardKeyForClassWithoutInheritance() + { + $cm = new ClassMetadataInfo('stdClass'); + $cm->setShardKey(array('id' => 'asc')); + + $shardKey = $cm->getShardKey(); + + $this->assertEquals(array('id' => 1), $shardKey['keys']); + } + + public function testSetShardKeyForClassWithSingleCollectionInheritance() + { + $cm = new ClassMetadataInfo('stdClass'); + $cm->inheritanceType = ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION; + $cm->setShardKey(array('id' => 'asc')); + + $shardKey = $cm->getShardKey(); + + $this->assertEquals(array('id' => 1), $shardKey['keys']); + } + + /** + * @expectedException \Doctrine\ODM\MongoDB\Mapping\MappingException + * @expectedExceptionMessage Shard key overriding in subclass is forbidden for single collection inheritance + */ + public function testSetShardKeyForClassWithSingleCollectionInheritanceWhichAlreadyHasIt() + { + $cm = new ClassMetadataInfo('stdClass'); + $cm->setShardKey(array('id' => 'asc')); + $cm->inheritanceType = ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_COLLECTION; + + $cm->setShardKey(array('foo' => 'asc')); + } + + public function testSetShardKeyForClassWithCollPerClassInheritance() + { + $cm = new ClassMetadataInfo('stdClass'); + $cm->inheritanceType = ClassMetadataInfo::INHERITANCE_TYPE_COLLECTION_PER_CLASS; + $cm->setShardKey(array('id' => 'asc')); + + $shardKey = $cm->getShardKey(); + + $this->assertEquals(array('id' => 1), $shardKey['keys']); + } + + public function testIsNotShardedIfThereIsNoShardKey() + { + $cm = new ClassMetadataInfo('stdClass'); + + $this->assertFalse($cm->isSharded()); + } + + public function testIsShardedIfThereIsAShardKey() + { + $cm = new ClassMetadataInfo('stdClass'); + $cm->setShardKey(array('id' => 'asc')); + + $this->assertTrue($cm->isSharded()); + } + + /** + * @expectedException \Doctrine\ODM\MongoDB\Mapping\MappingException + * @expectedExceptionMessage Embedded document can't have shard key: stdClass + */ + public function testEmbeddedDocumentCantHaveShardKey() + { + $cm = new ClassMetadataInfo('stdClass'); + $cm->isEmbeddedDocument = true; + $cm->setShardKey(array('id' => 'asc')); + } + + /** + * @expectedException \Doctrine\ODM\MongoDB\Mapping\MappingException + * @expectedExceptionMessage No increment fields allowed in the shard key + */ + public function testNoIncrementFieldsAllowedInShardKey() + { + $cm = new ClassMetadataInfo('stdClass'); + $cm->mapField(array( + 'fieldName' => 'inc', + 'type' => 'increment' + )); + $cm->setShardKey(array('inc')); + } } class TestCustomRepositoryClass extends DocumentRepository @@ -344,7 +429,7 @@ class EmbeddedAssociationsCascadeTest { /** @ODM\Id */ public $id; - + /** @ODM\EmbedOne(targetDocument="Documents\Address") */ public $address; diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php index df122515b1..b420737b3d 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ClassMetadataTest.php @@ -4,7 +4,6 @@ use Doctrine\ODM\MongoDB\Mapping\ClassMetadata; use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo; -use Doctrine\ODM\MongoDB\Events; class ClassMetadataTest extends \Doctrine\ODM\MongoDB\Tests\BaseTest { @@ -31,6 +30,7 @@ public function testClassMetadataInstanceSerialization() $cm->setFile('customFileProperty'); $cm->setDistance('customDistanceProperty'); $cm->setSlaveOkay(true); + $cm->setShardKey(array('_id' => '1')); $cm->setCollectionCapped(true); $cm->setCollectionMax(1000); $cm->setCollectionSize(500); @@ -57,6 +57,7 @@ public function testClassMetadataInstanceSerialization() $this->assertEquals('customFileProperty', $cm->file); $this->assertEquals('customDistanceProperty', $cm->distance); $this->assertTrue($cm->slaveOkay); + $this->assertEquals(array('keys' => array('_id' => 1), 'options' => array()), $cm->getShardKey()); $mapping = $cm->getFieldMapping('phonenumbers'); $this->assertEquals('Documents\Bar', $mapping['targetDocument']); $this->assertTrue($cm->getCollectionCapped()); diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ShardKeyInheritanceMappingTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ShardKeyInheritanceMappingTest.php new file mode 100644 index 0000000000..5324863a39 --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/ShardKeyInheritanceMappingTest.php @@ -0,0 +1,127 @@ +<?php + +namespace Doctrine\ODM\MongoDB\Tests\Mapping; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadataFactory; +use Doctrine\ODM\MongoDB\Tests\BaseTest; + +class ShardKeyInheritanceMappingTest extends BaseTest +{ + private $factory; + + public function setUp() + { + parent::setUp(); + $this->factory = new ClassMetadataFactory(); + $this->factory->setDocumentManager($this->dm); + $this->factory->setConfiguration($this->dm->getConfiguration()); + } + + + public function testShardKeyFromMappedSuperclass() + { + $class = $this->factory->getMetadataFor(__NAMESPACE__ . '\\ShardedSubclass'); + + $this->assertTrue($class->isSharded()); + $this->assertEquals(array('keys' => array('_id' => 1), 'options' => array()), $class->getShardKey()); + } + + public function testShardKeySingleCollectionInheritance() + { + $class = $this->factory->getMetadataFor(__NAMESPACE__ . '\\ShardedSingleCollInheritance2'); + + $this->assertTrue($class->isSharded()); + $this->assertEquals(array('keys' => array('_id' => 1), 'options' => array()), $class->getShardKey()); + } + + /** + * @expectedException Doctrine\ODM\MongoDB\Mapping\MappingException + */ + public function testShardKeySingleCollectionInheritanceOverriding() + { + $this->factory->getMetadataFor(__NAMESPACE__ . '\\ShardedSingleCollInheritance3'); + } + + public function testShardKeyCollectionPerClassInheritance() + { + $class = $this->factory->getMetadataFor(__NAMESPACE__ . '\\ShardedCollectionPerClass2'); + + $this->assertTrue($class->isSharded()); + $this->assertEquals(array('keys' => array('_id' => 1), 'options' => array()), $class->getShardKey()); + } + + public function testShardKeyCollectionPerClassInheritanceOverriding() + { + $class = $this->factory->getMetadataFor(__NAMESPACE__ . '\\ShardedCollectionPerClass3'); + + $this->assertTrue($class->isSharded()); + $this->assertEquals(array('keys' => array('_id' => 'hashed'), 'options' => array()), $class->getShardKey()); + } +} + + +/** + * @ODM\MappedSuperclass + * @ODM\ShardKey(keys={"_id"="asc"}) + */ +class ShardedSuperclass +{ + /** @ODM\String */ + private $name; +} + +/** @ODM\Document */ +class ShardedSubclass extends ShardedSuperclass +{ + /** @ODM\Id */ + private $id; +} + +/** + * @ODM\Document + * @ODM\InheritanceType("SINGLE_COLLECTION") + * @ODM\ShardKey(keys={"_id"="asc"}) + */ +class ShardedSingleCollInheritance1 +{ + /** @ODM\Id */ + private $id; +} + +/** + * @ODM\Document + */ +class ShardedSingleCollInheritance2 extends ShardedSingleCollInheritance1 +{} + +/** + * @ODM\Document + * @ODM\ShardKey(keys={"_id"="hashed"}) + */ +class ShardedSingleCollInheritance3 extends ShardedSingleCollInheritance1 +{} + +/** + * @ODM\Document + * @ODM\InheritanceType("COLLECTION_PER_CLASS") + * @ODM\ShardKey(keys={"_id"="asc"}) + */ +class ShardedCollectionPerClass1 +{ + /** @ODM\Id */ + private $id; +} + +/** + * @ODM\Document + */ +class ShardedCollectionPerClass2 extends ShardedCollectionPerClass1 +{} + +/** + * @ODM\Document + * @ODM\ShardKey(keys={"_id"="hashed"}) + */ +class ShardedCollectionPerClass3 extends ShardedCollectionPerClass1 +{} \ No newline at end of file diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/XmlMappingDriverTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/XmlMappingDriverTest.php index e92cc67a1e..89865d2b4c 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/XmlMappingDriverTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/XmlMappingDriverTest.php @@ -2,6 +2,7 @@ namespace Doctrine\ODM\MongoDB\Tests\Mapping; +use Doctrine\ODM\MongoDB\Mapping\ClassMetadataInfo; use Doctrine\ODM\MongoDB\Mapping\Driver\XmlDriver; class XmlMappingDriverTest extends AbstractMappingDriverTest @@ -10,4 +11,21 @@ protected function _loadDriver() { return new XmlDriver(__DIR__ . DIRECTORY_SEPARATOR . 'xml'); } + + public function testSetShardKeyOptionsByAttributes() + { + $class = new ClassMetadataInfo('doc'); + $driver = $this->_loadDriver(); + $element = new \SimpleXmlElement('<shard-key unique="true" numInitialChunks="4096"><key name="_id"/></shard-key>'); + + /** @uses XmlDriver::setShardKey */ + $m = new \ReflectionMethod(get_class($driver), 'setShardKey'); + $m->setAccessible(true); + $m->invoke($driver, $class, $element); + + $this->assertTrue($class->isSharded()); + $shardKey = $class->getShardKey(); + $this->assertSame(array('unique' => true, 'numInitialChunks' => 4096), $shardKey['options']); + $this->assertSame(array('_id' => 1), $shardKey['keys']); + } } \ No newline at end of file diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/xml/Doctrine.ODM.MongoDB.Tests.Mapping.AbstractMappingDriverUser.dcm.xml b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/xml/Doctrine.ODM.MongoDB.Tests.Mapping.AbstractMappingDriverUser.dcm.xml index 25e74305b0..2a3cd81c6d 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/xml/Doctrine.ODM.MongoDB.Tests.Mapping.AbstractMappingDriverUser.dcm.xml +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/xml/Doctrine.ODM.MongoDB.Tests.Mapping.AbstractMappingDriverUser.dcm.xml @@ -67,5 +67,10 @@ <lifecycle-callback method="doOtherStuffOnPrePersistToo" type="prePersist" /> <lifecycle-callback method="doStuffOnPostPersist" type="postPersist" /> </lifecycle-callbacks> + <shard-key> + <key name="name" order="asc"/> + <option name="unique" value="true"/> + <option name="numInitialChunks" value="4096"/> + </shard-key> </document> </doctrine-mongo-mapping> diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/yaml/Doctrine.ODM.MongoDB.Tests.Mapping.AbstractMappingDriverUser.dcm.yml b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/yaml/Doctrine.ODM.MongoDB.Tests.Mapping.AbstractMappingDriverUser.dcm.yml index 5291669693..e5718d4e34 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/Mapping/yaml/Doctrine.ODM.MongoDB.Tests.Mapping.AbstractMappingDriverUser.dcm.yml +++ b/tests/Doctrine/ODM/MongoDB/Tests/Mapping/yaml/Doctrine.ODM.MongoDB.Tests.Mapping.AbstractMappingDriverUser.dcm.yml @@ -46,6 +46,12 @@ Doctrine\ODM\MongoDB\Tests\Mapping\AbstractMappingDriverUser: options: unique: true dropDups: true + shardKey: + keys: + name: asc + options: + unique: true + numInitialChunks: 4096 referenceOne: address: targetDocument: Address diff --git a/tests/Doctrine/ODM/MongoDB/Tests/Persisters/DocumentPersisterGetShardKeyQueryTest.php b/tests/Doctrine/ODM/MongoDB/Tests/Persisters/DocumentPersisterGetShardKeyQueryTest.php new file mode 100644 index 0000000000..891865267d --- /dev/null +++ b/tests/Doctrine/ODM/MongoDB/Tests/Persisters/DocumentPersisterGetShardKeyQueryTest.php @@ -0,0 +1,113 @@ +<?php + +namespace Doctrine\ODM\MongoDB\Tests\Persisters; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; +use Doctrine\ODM\MongoDB\Persisters\DocumentPersister; +use Doctrine\ODM\MongoDB\Tests\BaseTest; + +class DocumentPersisterGetShardKeyQueryTest extends BaseTest +{ + public function testGetShardKeyQueryScalars() + { + $o = new ShardedByScalars(); + $o->int = 1; + $o->string = 'hi'; + $o->bool = true; + $o->float = 1.2; + + /** @var DocumentPersister $persister */ + $persister = $this->uow->getDocumentPersister(get_class($o)); + + $this->assertSame( + array('int' => $o->int, 'string' => $o->string, 'bool' => $o->bool, 'float' => $o->float), + $persister->getShardKeyQuery($o) + ); + } + + public function testGetShardKeyQueryObjects() + { + $o = new ShardedByObjects(); + $o->oid = '54ca2c4c81fec698130041a7'; + $o->bin = 'hi'; + $o->date = new \DateTime(); + + /** @var DocumentPersister $persister */ + $persister = $this->uow->getDocumentPersister(get_class($o)); + + $shardKeyQuery = $persister->getShardKeyQuery($o); + + $this->assertInstanceOf('MongoId', $shardKeyQuery['oid']); + $this->assertSame($o->oid, $shardKeyQuery['oid']->{'$id'}); + + $this->assertInstanceOf('MongoBinData', $shardKeyQuery['bin']); + $this->assertSame($o->bin, $shardKeyQuery['bin']->bin); + + $this->assertInstanceOf('MongoDate', $shardKeyQuery['date']); + $this->assertSame($o->date->getTimestamp(), $shardKeyQuery['date']->sec); + $this->assertSame(0, $shardKeyQuery['date']->usec); + } + + public function testShardById() + { + $o = new ShardedById(); + $o->identifier = new \MongoId(); + + /** @var DocumentPersister $persister */ + $persister = $this->uow->getDocumentPersister(get_class($o)); + $shardKeyQuery = $persister->getShardKeyQuery($o); + + $this->assertSame(array('_id' => $o->identifier), $shardKeyQuery); + } +} + +/** + * @ODM\Document + * @ODM\ShardKey(keys={"int"="asc","string"="asc","bool"="asc","float"="asc"}) + */ +class ShardedByScalars +{ + /** @ODM\Id */ + public $id; + + /** @ODM\Int */ + public $int; + + /** @ODM\String */ + public $string; + + /** @ODM\Boolean */ + public $bool; + + /** @ODM\Float */ + public $float; +} + +/** + * @ODM\Document + * @ODM\ShardKey(keys={"oid"="asc","bin"="asc","date"="asc"}) + */ +class ShardedByObjects +{ + /** @ODM\Id */ + public $id; + + /** @ODM\ObjectId */ + public $oid; + + /** @ODM\Bin */ + public $bin; + + /** @ODM\Date */ + public $date; +} + +/** + * @ODM\Document + * @ODM\ShardKey(keys={"_id"="asc"}) + */ +class ShardedById +{ + /** @ODM\Id */ + public $identifier; +} \ No newline at end of file diff --git a/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php b/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php index dde3c2753b..c32b6b5d1b 100644 --- a/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php +++ b/tests/Doctrine/ODM/MongoDB/Tests/SchemaManagerTest.php @@ -295,6 +295,149 @@ public function testDropDatabases() $this->schemaManager->dropDatabases(); } + public function testEnsureDocumentSharding() + { + $dbName = DOCTRINE_MONGODB_DATABASE; + $classMetadata = $this->dm->getClassMetadata('Documents\Sharded\ShardedUser'); + $collectionName = $classMetadata->getCollection(); + $dbMock = $this->getMockDatabase(); + $dbMock->method('getName')->willReturn($dbName); + $adminDBMock = $this->getMockDatabase(); + $connMock = $this->getMockConnection(); + $connMock->method('selectDatabase')->with('admin')->willReturn($adminDBMock); + $this->dm->connection = $connMock; + $this->dm->documentDatabases = array($classMetadata->getName() => $dbMock); + + $adminDBMock + ->expects($this->at(0)) + ->method('command') + ->with(array('enableSharding' => $dbName)) + ->willReturn(array('ok' => 1)); + $adminDBMock + ->expects($this->at(1)) + ->method('command') + ->with(array('shardCollection' => $dbName . '.' . $collectionName, 'key' => array('_id' => 'hashed'))) + ->willReturn(array('ok' => 1)); + + $this->schemaManager->ensureDocumentSharding($classMetadata->getName()); + } + + /** + * @expectedException \Doctrine\ODM\MongoDB\MongoDBException + * @expectedExceptionMessage Failed to ensure sharding for document + */ + public function testEnsureDocumentShardingThrowsExceptionIfThereWasAnError() + { + $dbName = DOCTRINE_MONGODB_DATABASE; + $classMetadata = $this->dm->getClassMetadata('Documents\Sharded\ShardedUser'); + $collectionName = $classMetadata->getCollection(); + $dbMock = $this->getMockDatabase(); + $dbMock->method('getName')->willReturn($dbName); + $adminDBMock = $this->getMockDatabase(); + $connMock = $this->getMockConnection(); + $connMock->method('selectDatabase')->with('admin')->willReturn($adminDBMock); + $this->dm->connection = $connMock; + $this->dm->documentDatabases = array($classMetadata->getName() => $dbMock); + + $adminDBMock + ->expects($this->at(0)) + ->method('command') + ->with(array('enableSharding' => $dbName)) + ->willReturn(array('ok' => 1)); + $adminDBMock + ->expects($this->at(1)) + ->method('command') + ->with(array('shardCollection' => $dbName . '.' . $collectionName, 'key' => array('_id' => 'hashed'))) + ->willReturn(array('ok' => 0, 'errmsg' => 'scary error')); + + $this->schemaManager->ensureDocumentSharding($classMetadata->getName()); + } + + public function testEnsureDocumentShardingIgnoresAlreadyShardedError() + { + $dbName = DOCTRINE_MONGODB_DATABASE; + $classMetadata = $this->dm->getClassMetadata('Documents\Sharded\ShardedUser'); + $collectionName = $classMetadata->getCollection(); + $dbMock = $this->getMockDatabase(); + $dbMock->method('getName')->willReturn($dbName); + $adminDBMock = $this->getMockDatabase(); + $connMock = $this->getMockConnection(); + $connMock->method('selectDatabase')->with('admin')->willReturn($adminDBMock); + $this->dm->connection = $connMock; + $this->dm->documentDatabases = array($classMetadata->getName() => $dbMock); + + $adminDBMock + ->expects($this->at(0)) + ->method('command') + ->with(array('enableSharding' => $dbName)) + ->willReturn(array('ok' => 1)); + $adminDBMock + ->expects($this->at(1)) + ->method('command') + ->with(array('shardCollection' => $dbName . '.' . $collectionName, 'key' => array('_id' => 'hashed'))) + ->willReturn(array('ok' => 0, 'errmsg' => 'already sharded')); + + $this->schemaManager->ensureDocumentSharding($classMetadata->getName()); + } + + public function testEnableShardingForDb() + { + $adminDBMock = $this->getMockDatabase(); + $adminDBMock + ->expects($this->once()) + ->method('command') + ->with(array('enableSharding' => 'db')) + ->willReturn(array('ok' => 1)); + $connMock = $this->getMockConnection(); + $connMock->method('selectDatabase')->with('admin')->willReturn($adminDBMock); + $this->dm->connection = $connMock; + $dbMock = $this->getMockDatabase(); + $dbMock->method('getName')->willReturn('db'); + $this->dm->documentDatabases = array('Documents\Sharded\ShardedUser' => $dbMock); + + $this->schemaManager->enableShardingForDbByDocumentName('Documents\Sharded\ShardedUser'); + } + + /** + * @expectedException \Doctrine\ODM\MongoDB\MongoDBException + * @expectedExceptionMessage Failed to enable sharding for database + */ + public function testEnableShardingForDbThrowsExceptionInCaseOfError() + { + $adminDBMock = $this->getMockDatabase(); + $adminDBMock + ->expects($this->once()) + ->method('command') + ->with(array('enableSharding' => 'db')) + ->willReturn(array('ok' => 0, 'errmsg' => 'scary error')); + $connMock = $this->getMockConnection(); + $connMock->method('selectDatabase')->with('admin')->willReturn($adminDBMock); + $this->dm->connection = $connMock; + $dbMock = $this->getMockDatabase(); + $dbMock->method('getName')->willReturn('db'); + $this->dm->documentDatabases = array('Documents\Sharded\ShardedUser' => $dbMock); + + $this->schemaManager->enableShardingForDbByDocumentName('Documents\Sharded\ShardedUser'); + } + + public function testEnableShardingForDbIgnoresAlreadyShardedError() + { + $adminDBMock = $this->getMockDatabase(); + $adminDBMock + ->expects($this->once()) + ->method('command') + ->with(array('enableSharding' => 'db')) + ->willReturn(array('ok' => 0, 'errmsg' => 'already enabled')); + $connMock = $this->getMockConnection(); + $connMock->method('selectDatabase')->with('admin')->willReturn($adminDBMock); + $this->dm->connection = $connMock; + $dbMock = $this->getMockDatabase(); + $dbMock->method('getName')->willReturn('db'); + $this->dm->documentDatabases = array('Documents\Sharded\ShardedUser' => $dbMock); + + $this->schemaManager->enableShardingForDbByDocumentName('Documents\Sharded\ShardedUser'); + } + private function getMockCollection() { return $this->getMockBuilder('Doctrine\MongoDB\Collection') @@ -345,4 +488,11 @@ private function getMockUnitOfWork() return $uow; } + + private function getMockConnection() + { + return $this->getMockBuilder('Doctrine\MongoDB\Connection') + ->disableOriginalConstructor() + ->getMock(); + } } diff --git a/tests/Documents/Sharded/ShardedOne.php b/tests/Documents/Sharded/ShardedOne.php new file mode 100644 index 0000000000..0fa5aaf89c --- /dev/null +++ b/tests/Documents/Sharded/ShardedOne.php @@ -0,0 +1,21 @@ +<?php + +namespace Documents\Sharded; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * @ODM\Document(collection="sharded.one") + * @ODM\ShardKey(keys={"k"="asc"}) + */ +class ShardedOne +{ + /** @ODM\Id */ + public $id; + + /** @ODM\String */ + public $title = 'test'; + + /** @ODM\String(name="k") */ + public $key = 'testing'; +} \ No newline at end of file diff --git a/tests/Documents/Sharded/ShardedUser.php b/tests/Documents/Sharded/ShardedUser.php new file mode 100644 index 0000000000..5aca4c9997 --- /dev/null +++ b/tests/Documents/Sharded/ShardedUser.php @@ -0,0 +1,18 @@ +<?php + +namespace Documents\Sharded; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * @ODM\Document(collection="sharded.users") + * @ODM\ShardKey(keys={"_id"="hashed"}) + */ +class ShardedUser +{ + /** @ODM\Id */ + public $id; + + /** @ODM\String */ + public $name; +} \ No newline at end of file