diff --git a/docs/en/reference/annotations-reference.rst b/docs/en/reference/annotations-reference.rst index f746cc23e2d..22686dbe7f0 100644 --- a/docs/en/reference/annotations-reference.rst +++ b/docs/en/reference/annotations-reference.rst @@ -513,7 +513,8 @@ Required attributes: - **name**: Name of the Index -- **columns**: Array of columns. +- **fields**: Array of fields. Exactly one of **fields**, **columns** is required. +- **columns**: Array of columns. Exactly one of **fields**, **columns** is required. Optional attributes: @@ -535,6 +536,19 @@ Basic example: { } +Basic example using fields: + +.. code-block:: php + + - + + @@ -351,6 +352,7 @@ + diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index af03bf39171..6810aca9c44 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -35,6 +35,7 @@ use function class_exists; use function constant; +use function count; use function defined; use function get_class; use function is_array; @@ -110,7 +111,28 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) if ($tableAnnot->indexes !== null) { foreach ($tableAnnot->indexes as $indexAnnot) { - $index = ['columns' => $indexAnnot->columns]; + $index = []; + + if (! empty($indexAnnot->columns)) { + $index['columns'] = $indexAnnot->columns; + } + + if (! empty($indexAnnot->fields)) { + $index['fields'] = $indexAnnot->fields; + } + + if ( + isset($index['columns'], $index['fields']) + || ( + ! isset($index['columns']) + && ! isset($index['fields']) + ) + ) { + throw MappingException::invalidIndexConfiguration( + $className, + (string) ($indexAnnot->name ?? count($primaryTable['indexes'])) + ); + } if (! empty($indexAnnot->flags)) { $index['flags'] = $indexAnnot->flags; @@ -130,7 +152,28 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) if ($tableAnnot->uniqueConstraints !== null) { foreach ($tableAnnot->uniqueConstraints as $uniqueConstraintAnnot) { - $uniqueConstraint = ['columns' => $uniqueConstraintAnnot->columns]; + $uniqueConstraint = []; + + if (! empty($uniqueConstraintAnnot->columns)) { + $uniqueConstraint['columns'] = $uniqueConstraintAnnot->columns; + } + + if (! empty($uniqueConstraintAnnot->fields)) { + $uniqueConstraint['fields'] = $uniqueConstraintAnnot->fields; + } + + if ( + isset($uniqueConstraint['columns'], $uniqueConstraint['fields']) + || ( + ! isset($uniqueConstraint['columns']) + && ! isset($uniqueConstraint['fields']) + ) + ) { + throw MappingException::invalidUniqueConstraintConfiguration( + $className, + (string) ($uniqueConstraintAnnot->name ?? count($primaryTable['uniqueConstraints'])) + ); + } if (! empty($uniqueConstraintAnnot->options)) { $uniqueConstraint['options'] = $uniqueConstraintAnnot->options; diff --git a/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php index 591203d776e..06d11bef944 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AttributeDriver.php @@ -18,6 +18,7 @@ use function assert; use function class_exists; use function constant; +use function count; use function defined; class AttributeDriver extends AnnotationDriver @@ -76,7 +77,28 @@ public function loadMetadataForClass($className, ClassMetadata $metadata): void if (isset($classAttributes[Mapping\Index::class])) { foreach ($classAttributes[Mapping\Index::class] as $indexAnnot) { - $index = ['columns' => $indexAnnot->columns]; + $index = []; + + if (! empty($indexAnnot->columns)) { + $index['columns'] = $indexAnnot->columns; + } + + if (! empty($indexAnnot->fields)) { + $index['fields'] = $indexAnnot->fields; + } + + if ( + isset($index['columns'], $index['fields']) + || ( + ! isset($index['columns']) + && ! isset($index['fields']) + ) + ) { + throw MappingException::invalidIndexConfiguration( + $className, + (string) ($indexAnnot->name ?? count($primaryTable['indexes'])) + ); + } if (! empty($indexAnnot->flags)) { $index['flags'] = $indexAnnot->flags; @@ -96,7 +118,28 @@ public function loadMetadataForClass($className, ClassMetadata $metadata): void if (isset($classAttributes[Mapping\UniqueConstraint::class])) { foreach ($classAttributes[Mapping\UniqueConstraint::class] as $uniqueConstraintAnnot) { - $uniqueConstraint = ['columns' => $uniqueConstraintAnnot->columns]; + $uniqueConstraint = []; + + if (! empty($uniqueConstraintAnnot->columns)) { + $uniqueConstraint['columns'] = $uniqueConstraintAnnot->columns; + } + + if (! empty($uniqueConstraintAnnot->fields)) { + $uniqueConstraint['fields'] = $uniqueConstraintAnnot->fields; + } + + if ( + isset($uniqueConstraint['columns'], $uniqueConstraint['fields']) + || ( + ! isset($uniqueConstraint['columns']) + && ! isset($uniqueConstraint['fields']) + ) + ) { + throw MappingException::invalidUniqueConstraintConfiguration( + $className, + (string) ($uniqueConstraintAnnot->name ?? count($primaryTable['uniqueConstraints'])) + ); + } if (! empty($uniqueConstraintAnnot->options)) { $uniqueConstraint['options'] = $uniqueConstraintAnnot->options; diff --git a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php index d8a122b1094..bbd20f40f09 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php @@ -31,6 +31,7 @@ use function assert; use function constant; +use function count; use function defined; use function explode; use function file_get_contents; @@ -212,7 +213,28 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) if (isset($xmlRoot->indexes)) { $metadata->table['indexes'] = []; foreach ($xmlRoot->indexes->index as $indexXml) { - $index = ['columns' => explode(',', (string) $indexXml['columns'])]; + $index = []; + + if (isset($indexXml['columns']) && ! empty($indexXml['columns'])) { + $index['columns'] = explode(',', (string) $indexXml['columns']); + } + + if (isset($indexXml['fields'])) { + $index['fields'] = explode(',', (string) $indexXml['fields']); + } + + if ( + isset($index['columns'], $index['fields']) + || ( + ! isset($index['columns']) + && ! isset($index['fields']) + ) + ) { + throw MappingException::invalidIndexConfiguration( + $className, + (string) ($indexXml['name'] ?? count($metadata->table['indexes'])) + ); + } if (isset($indexXml['flags'])) { $index['flags'] = explode(',', (string) $indexXml['flags']); @@ -234,7 +256,28 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) if (isset($xmlRoot->{'unique-constraints'})) { $metadata->table['uniqueConstraints'] = []; foreach ($xmlRoot->{'unique-constraints'}->{'unique-constraint'} as $uniqueXml) { - $unique = ['columns' => explode(',', (string) $uniqueXml['columns'])]; + $unique = []; + + if (isset($uniqueXml['columns']) && ! empty($uniqueXml['columns'])) { + $unique['columns'] = explode(',', (string) $uniqueXml['columns']); + } + + if (isset($uniqueXml['fields'])) { + $unique['fields'] = explode(',', (string) $uniqueXml['fields']); + } + + if ( + isset($unique['columns'], $unique['fields']) + || ( + ! isset($unique['columns']) + && ! isset($unique['fields']) + ) + ) { + throw MappingException::invalidUniqueConstraintConfiguration( + $className, + (string) ($uniqueXml['name'] ?? count($metadata->table['uniqueConstraints'])) + ); + } if (isset($uniqueXml->options)) { $unique['options'] = $this->parseOptions($uniqueXml->options->children()); diff --git a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php index 20f8ace707e..cb139e4145b 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php @@ -228,10 +228,35 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) $indexYml['name'] = $name; } - if (is_string($indexYml['columns'])) { - $index = ['columns' => array_map('trim', explode(',', $indexYml['columns']))]; - } else { - $index = ['columns' => $indexYml['columns']]; + $index = []; + + if (isset($indexYml['columns'])) { + if (is_string($indexYml['columns'])) { + $index['columns'] = array_map('trim', explode(',', $indexYml['columns'])); + } else { + $index['columns'] = $indexYml['columns']; + } + } + + if (isset($indexYml['fields'])) { + if (is_string($indexYml['fields'])) { + $index['fields'] = array_map('trim', explode(',', $indexYml['fields'])); + } else { + $index['fields'] = $indexYml['fields']; + } + } + + if ( + isset($index['columns'], $index['fields']) + || ( + ! isset($index['columns']) + && ! isset($index['fields']) + ) + ) { + throw MappingException::invalidIndexConfiguration( + $className, + $indexYml['name'] + ); } if (isset($indexYml['flags'])) { @@ -257,10 +282,35 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) $uniqueYml['name'] = $name; } - if (is_string($uniqueYml['columns'])) { - $unique = ['columns' => array_map('trim', explode(',', $uniqueYml['columns']))]; - } else { - $unique = ['columns' => $uniqueYml['columns']]; + $unique = []; + + if (isset($uniqueYml['columns'])) { + if (is_string($uniqueYml['columns'])) { + $unique['columns'] = array_map('trim', explode(',', $uniqueYml['columns'])); + } else { + $unique['columns'] = $uniqueYml['columns']; + } + } + + if (isset($uniqueYml['fields'])) { + if (is_string($uniqueYml['fields'])) { + $unique['fields'] = array_map('trim', explode(',', $uniqueYml['fields'])); + } else { + $unique['fields'] = $uniqueYml['fields']; + } + } + + if ( + isset($unique['columns'], $unique['fields']) + || ( + ! isset($unique['columns']) + && ! isset($unique['fields']) + ) + ) { + throw MappingException::invalidUniqueConstraintConfiguration( + $className, + $uniqueYml['name'] + ); } if (isset($uniqueYml['options'])) { diff --git a/lib/Doctrine/ORM/Mapping/Index.php b/lib/Doctrine/ORM/Mapping/Index.php index 3ed3339cf90..99f5ff63678 100644 --- a/lib/Doctrine/ORM/Mapping/Index.php +++ b/lib/Doctrine/ORM/Mapping/Index.php @@ -37,6 +37,9 @@ final class Index implements Annotation /** @var array */ public $columns; + /** @var array */ + public $fields; + /** @var array */ public $flags; @@ -45,16 +48,19 @@ final class Index implements Annotation /** * @param array $columns + * @param array $fields * @param array $flags * @param array $options */ public function __construct( - array $columns, + ?array $columns = null, + ?array $fields = null, ?string $name = null, ?array $flags = null, ?array $options = null ) { $this->columns = $columns; + $this->fields = $fields; $this->name = $name; $this->flags = $flags; $this->options = $options; diff --git a/lib/Doctrine/ORM/Mapping/MappingException.php b/lib/Doctrine/ORM/Mapping/MappingException.php index a23fdd8c298..3933f7bfdbc 100644 --- a/lib/Doctrine/ORM/Mapping/MappingException.php +++ b/lib/Doctrine/ORM/Mapping/MappingException.php @@ -933,4 +933,32 @@ public static function illegalOverrideOfInheritedProperty($className, $propertyN ) ); } + + /** + * @return self + */ + public static function invalidIndexConfiguration($className, $indexName) + { + return new self( + sprintf( + 'Index %s for entity %s should contain columns or fields values, but not both.', + $indexName, + $className + ) + ); + } + + /** + * @return self + */ + public static function invalidUniqueConstraintConfiguration($className, $indexName) + { + return new self( + sprintf( + 'Unique constraint %s for entity %s should contain columns or fields values, but not both.', + $indexName, + $className + ) + ); + } } diff --git a/lib/Doctrine/ORM/Mapping/UniqueConstraint.php b/lib/Doctrine/ORM/Mapping/UniqueConstraint.php index f91467ec208..bb30b5b94a0 100644 --- a/lib/Doctrine/ORM/Mapping/UniqueConstraint.php +++ b/lib/Doctrine/ORM/Mapping/UniqueConstraint.php @@ -37,20 +37,26 @@ final class UniqueConstraint implements Annotation /** @var array */ public $columns; + /** @var array */ + public $fields; + /** @var array */ public $options; /** * @param array $columns + * @param array $fields * @param array $options */ public function __construct( ?string $name = null, ?array $columns = null, + ?array $fields = null, ?array $options = null ) { $this->name = $name; $this->columns = $columns; + $this->fields = $fields; $this->options = $options; } } diff --git a/lib/Doctrine/ORM/Tools/SchemaTool.php b/lib/Doctrine/ORM/Tools/SchemaTool.php index ec0aae5cbe6..2ccbede124c 100644 --- a/lib/Doctrine/ORM/Tools/SchemaTool.php +++ b/lib/Doctrine/ORM/Tools/SchemaTool.php @@ -30,6 +30,7 @@ use Doctrine\DBAL\Schema\Visitor\RemoveNamespacedAssets; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\MappingException; use Doctrine\ORM\Mapping\QuoteStrategy; use Doctrine\ORM\ORMException; use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; @@ -137,6 +138,49 @@ private function processingNotRequired( ($class->isInheritanceTypeSingleTable() && $class->name !== $class->rootEntityName); } + /** + * Resolves fields in index mapping to column names + * + * @param mixed[] $indexData index or unique constraint data + * + * @return string[] Column names from combined fields and columns mappings + */ + private function getIndexColumns(ClassMetadata $class, array $indexData): array + { + $columns = []; + + if ( + isset($indexData['columns'], $indexData['fields']) + || ( + ! isset($indexData['columns']) + && ! isset($indexData['fields']) + ) + ) { + throw MappingException::invalidIndexConfiguration( + $class, + $indexData['name'] ?? 'unnamed' + ); + } + + if (isset($indexData['columns'])) { + $columns = $indexData['columns']; + } + + if (isset($indexData['fields'])) { + foreach ($indexData['fields'] as $fieldName) { + if ($class->hasField($fieldName)) { + $columns[] = $this->quoteStrategy->getColumnName($fieldName, $class, $this->platform); + } elseif ($class->hasAssociation($fieldName)) { + foreach ($class->getAssociationMapping($fieldName)['joinColumns'] as $joinColumn) { + $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); + } + } + } + } + + return $columns; + } + /** * Creates a Schema instance from a given set of metadata classes. * @@ -309,13 +353,18 @@ static function (ClassMetadata $class) use ($idMapping): bool { $indexData['flags'] = []; } - $table->addIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, (array) $indexData['flags'], $indexData['options'] ?? []); + $table->addIndex( + $this->getIndexColumns($class, $indexData), + is_numeric($indexName) ? null : $indexName, + (array) $indexData['flags'], + $indexData['options'] ?? [] + ); } } if (isset($class->table['uniqueConstraints'])) { foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) { - $uniqIndex = new Index($indexName, $indexData['columns'], true, false, [], $indexData['options'] ?? []); + $uniqIndex = new Index($indexName, $this->getIndexColumns($class, $indexData), true, false, [], $indexData['options'] ?? []); foreach ($table->getIndexes() as $tableIndexName => $tableIndex) { if ($tableIndex->isFullfilledBy($uniqIndex)) { @@ -324,7 +373,7 @@ static function (ClassMetadata $class) use ($idMapping): bool { } } - $table->addUniqueIndex($indexData['columns'], is_numeric($indexName) ? null : $indexName, $indexData['options'] ?? []); + $table->addUniqueIndex($uniqIndex->getColumns(), is_numeric($indexName) ? null : $indexName, $indexData['options'] ?? []); } } diff --git a/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php index 76644dc2190..e4b0463a486 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php @@ -104,6 +104,7 @@ public function testEntityIndexes(ClassMetadata $class): ClassMetadata [ 'name_idx' => ['columns' => ['name']], 0 => ['columns' => ['user_email']], + 'fields' => ['fields' => ['name', 'email']], ], $class->table['indexes'] ); @@ -111,6 +112,12 @@ public function testEntityIndexes(ClassMetadata $class): ClassMetadata return $class; } + public function testEntityIncorrectIndexes(): void + { + $this->expectException(MappingException::class); + $this->createClassMetadata(UserIncorrectIndex::class); + } + public function testEntityIndexFlagsAndPartialIndexes(): void { $class = $this->createClassMetadata(Comment::class); @@ -141,6 +148,7 @@ public function testEntityUniqueConstraints(ClassMetadata $class): ClassMetadata $this->assertEquals( [ 'search_idx' => ['columns' => ['name', 'user_email'], 'options' => ['where' => 'name IS NOT NULL']], + 'phone_idx' => ['fields' => ['name', 'phone']], ], $class->table['uniqueConstraints'] ); @@ -148,6 +156,12 @@ public function testEntityUniqueConstraints(ClassMetadata $class): ClassMetadata return $class; } + public function testEntityIncorrectUniqueContraint(): void + { + $this->expectException(MappingException::class); + $this->createClassMetadata(UserIncorrectUniqueConstraint::class); + } + /** * @depends testEntityTableNameAndInheritance */ @@ -1067,16 +1081,16 @@ public function testDiscriminatorColumnDefaultName(): void * @HasLifecycleCallbacks * @Table( * name="cms_users", - * uniqueConstraints={@UniqueConstraint(name="search_idx", columns={"name", "user_email"}, options={"where": "name IS NOT NULL"})}, - * indexes={@Index(name="name_idx", columns={"name"}), @Index(name="0", columns={"user_email"})}, + * uniqueConstraints={@UniqueConstraint(name="search_idx", columns={"name", "user_email"}, options={"where": "name IS NOT NULL"}), @UniqueConstraint(name="phone_idx", fields={"name", "phone"})}, + * indexes={@Index(name="name_idx", columns={"name"}), @Index(name="0", columns={"user_email"}), @index(name="fields", fields={"name", "email"})}, * options={"foo": "bar", "baz": {"key": "val"}} * ) * @NamedQueries({@NamedQuery(name="all", query="SELECT u FROM __CLASS__ u")}) */ #[ORM\Entity(), ORM\HasLifecycleCallbacks()] #[ORM\Table(name: 'cms_users', options: ['foo' => 'bar', 'baz' => ['key' => 'val']])] -#[ORM\Index(name: 'name_idx', columns: ['name']), ORM\Index(name: '0', columns: ['user_email'])] -#[ORM\UniqueConstraint(name: 'search_idx', columns: ['name', 'user_email'], options: ['where' => 'name IS NOT NULL'])] +#[ORM\Index(name: 'name_idx', columns: ['name']), ORM\Index(name: '0', columns: ['user_email']), ORM\Index(name: 'fields', fields: ['name', 'email'])] +#[ORM\UniqueConstraint(name: 'search_idx', columns: ['name', 'user_email'], options: ['where' => 'name IS NOT NULL']), ORM\UniqueConstraint(name: 'phone_idx', fields: ['name', 'phone'])] class User { /** @@ -1287,10 +1301,12 @@ public static function loadMetadata(ClassMetadataInfo $metadata): void ); $metadata->table['uniqueConstraints'] = [ 'search_idx' => ['columns' => ['name', 'user_email'], 'options' => ['where' => 'name IS NOT NULL']], + 'phone_idx' => ['fields' => ['name', 'phone']], ]; $metadata->table['indexes'] = [ 'name_idx' => ['columns' => ['name']], 0 => ['columns' => ['user_email']], + 'fields' => ['fields' => ['name', 'email']], ]; $metadata->setSequenceGeneratorDefinition( [ @@ -1308,6 +1324,124 @@ public static function loadMetadata(ClassMetadataInfo $metadata): void } } +/** + * @Entity + * @Table( + * indexes={@Index(name="name_idx", columns={"name"}, fields={"email"})}, + * ) + */ +class UserIncorrectIndex +{ + /** + * @var int + * @Id + * @Column(type="integer") + * @generatedValue(strategy="AUTO") + **/ + public $id; + + /** + * @var string + * @Column + */ + public $name; + + /** + * @var string + * @Column(name="user_email") + */ + public $email; + + public static function loadMetadata(ClassMetadataInfo $metadata): void + { + $metadata->setInheritanceType(ClassMetadataInfo::INHERITANCE_TYPE_NONE); + $metadata->setPrimaryTable([]); + $metadata->mapField( + [ + 'id' => true, + 'fieldName' => 'id', + 'type' => 'integer', + 'columnName' => 'id', + ] + ); + $metadata->mapField( + [ + 'fieldName' => 'name', + 'type' => 'string', + ] + ); + $metadata->mapField( + [ + 'fieldName' => 'email', + 'type' => 'string', + 'columnName' => 'user_email', + ] + ); + $metadata->table['indexes'] = [ + 'name_idx' => ['columns' => ['name'], 'fields' => ['email']], + ]; + } +} + +/** + * @Entity + * @Table( + * uniqueConstraints={@UniqueConstraint(name="name_idx", columns={"name"}, fields={"email"})}, + * ) + */ +class UserIncorrectUniqueConstraint +{ + /** + * @var int + * @Id + * @Column(type="integer") + * @generatedValue(strategy="AUTO") + **/ + public $id; + + /** + * @var string + * @Column + */ + public $name; + + /** + * @var string + * @Column(name="user_email") + */ + public $email; + + public static function loadMetadata(ClassMetadataInfo $metadata): void + { + $metadata->setInheritanceType(ClassMetadataInfo::INHERITANCE_TYPE_NONE); + $metadata->setPrimaryTable([]); + $metadata->mapField( + [ + 'id' => true, + 'fieldName' => 'id', + 'type' => 'integer', + 'columnName' => 'id', + ] + ); + $metadata->mapField( + [ + 'fieldName' => 'name', + 'type' => 'string', + ] + ); + $metadata->mapField( + [ + 'fieldName' => 'email', + 'type' => 'string', + 'columnName' => 'user_email', + ] + ); + $metadata->table['uniqueConstraints'] = [ + 'name_idx' => ['columns' => ['name'], 'fields' => ['email']], + ]; + } +} + /** * @Entity * @InheritanceType("SINGLE_TABLE") diff --git a/tests/Doctrine/Tests/ORM/Mapping/PHPMappingDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/PHPMappingDriverTest.php index 476cc8c8228..5f0f520dd18 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/PHPMappingDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/PHPMappingDriverTest.php @@ -49,4 +49,14 @@ public function testFailingSecondLevelCacheAssociation(): void $class = new ClassMetadata(Mapping\PHPSLC::class); $mappingDriver->loadMetadataForClass(Mapping\PHPSLC::class, $class); } + + public function testEntityIncorrectIndexes(): void + { + self::markTestSkipped('PHP driver does not ensure index correctness'); + } + + public function testEntityIncorrectUniqueContraint(): void + { + self::markTestSkipped('PHP driver does not ensure index correctness'); + } } diff --git a/tests/Doctrine/Tests/ORM/Mapping/StaticPHPMappingDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/StaticPHPMappingDriverTest.php index b0d2e4193ee..ffb752908b3 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/StaticPHPMappingDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/StaticPHPMappingDriverTest.php @@ -45,4 +45,14 @@ public function testSchemaDefinitionViaSchemaDefinedInTableNameInTableAnnotation { $this->markTestIncomplete(); } + + public function testEntityIncorrectIndexes(): void + { + self::markTestSkipped('Static PHP driver does not ensure index correctness'); + } + + public function testEntityIncorrectUniqueContraint(): void + { + self::markTestSkipped('Static PHP driver does not ensure index correctness'); + } } diff --git a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php index 2d89ffed367..9c0a0fd2b27 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php +++ b/tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php @@ -130,10 +130,12 @@ ]; $metadata->table['uniqueConstraints'] = [ 'search_idx' => ['columns' => ['name', 'user_email'], 'options' => ['where' => 'name IS NOT NULL']], + 'phone_idx' => ['fields' => ['name', 'phone']], ]; $metadata->table['indexes'] = [ 'name_idx' => ['columns' => ['name']], 0 => ['columns' => ['user_email']], + 'fields' => ['fields' => ['name', 'email']], ]; $metadata->setSequenceGeneratorDefinition( [ diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml index 1fa06126334..5343d040e16 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml @@ -16,6 +16,7 @@ + @@ -24,6 +25,7 @@ + diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.UserIncorrectIndex.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.UserIncorrectIndex.dcm.xml new file mode 100644 index 00000000000..cad101ed502 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.UserIncorrectIndex.dcm.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.UserIncorrectUniqueConstraint.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.UserIncorrectUniqueConstraint.dcm.xml new file mode 100644 index 00000000000..000892c2785 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.UserIncorrectUniqueConstraint.dcm.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml index e3a32ce76be..d3504ffbaa0 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml +++ b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml @@ -78,8 +78,12 @@ Doctrine\Tests\ORM\Mapping\User: columns: name,user_email options: where: name IS NOT NULL + phone_idx: + fields: name,phone indexes: name_idx: columns: name 0: columns: user_email + fields: + fields: name,email diff --git a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.UserIncorrectIndex.dcm.yml b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.UserIncorrectIndex.dcm.yml new file mode 100644 index 00000000000..06855341204 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.UserIncorrectIndex.dcm.yml @@ -0,0 +1,17 @@ +Doctrine\Tests\ORM\Mapping\UserIncorrectIndex: + type: entity + id: + id: + type: integer + generator: + strategy: AUTO + fields: + name: + type: string + email: + type: string + column: user_email + indexes: + name_idx: + columns: name + fields: email diff --git a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.UserIncorrectUniqueConstraint.dcm.yml b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.UserIncorrectUniqueConstraint.dcm.yml new file mode 100644 index 00000000000..82f7e72a778 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.UserIncorrectUniqueConstraint.dcm.yml @@ -0,0 +1,17 @@ +Doctrine\Tests\ORM\Mapping\UserIncorrectUniqueConstraint: + type: entity + id: + id: + type: integer + generator: + strategy: AUTO + fields: + name: + type: string + email: + type: string + column: user_email + uniqueConstraints: + name_idx: + columns: name + fields: email diff --git a/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php b/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php index 4622c217673..87d7274e901 100644 --- a/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php @@ -6,10 +6,15 @@ use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\MappingException; +use Doctrine\ORM\Mapping\UnderscoreNamingStrategy; use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs; use Doctrine\ORM\Tools\SchemaTool; use Doctrine\ORM\Tools\ToolEvents; +use Doctrine\Persistence\Mapping\Driver\StaticPHPDriver; +use Doctrine\Persistence\Mapping\RuntimeReflectionService; use Doctrine\Tests\Models\CMS\CmsAddress; use Doctrine\Tests\Models\CMS\CmsArticle; use Doctrine\Tests\Models\CMS\CmsComment; @@ -277,6 +282,52 @@ public function testDerivedCompositeKey(): void self::assertSame($foreignColumns, $foreignKey->getForeignColumns()); } } + + public function testIndexesBasedOnFields(): void + { + $em = $this->getTestEntityManager(); + $em->getConfiguration()->setNamingStrategy(new UnderscoreNamingStrategy()); + + $schemaTool = new SchemaTool($em); + $metadata = $em->getClassMetadata(IndexByFieldEntity::class); + $schema = $schemaTool->getSchemaFromMetadata([$metadata]); + $table = $schema->getTable('field_index'); + + self::assertEquals(['index', 'field_name'], $table->getIndex('index')->getColumns()); + self::assertEquals(['index', 'table'], $table->getIndex('uniq')->getColumns()); + } + + public function testIncorrectIndexesBasedOnFields(): void + { + $em = $this->getTestEntityManager(); + $em->getConfiguration()->setNamingStrategy(new UnderscoreNamingStrategy()); + + $schemaTool = new SchemaTool($em); + $mappingDriver = new StaticPHPDriver([]); + $class = new ClassMetadata(IncorrectIndexByFieldEntity::class); + + $class->initializeReflection(new RuntimeReflectionService()); + $mappingDriver->loadMetadataForClass(IncorrectIndexByFieldEntity::class, $class); + + $this->expectException(MappingException::class); + $schemaTool->getSchemaFromMetadata([$class]); + } + + public function testIncorrectUniqueConstraintsBasedOnFields(): void + { + $em = $this->getTestEntityManager(); + $em->getConfiguration()->setNamingStrategy(new UnderscoreNamingStrategy()); + + $schemaTool = new SchemaTool($em); + $mappingDriver = new StaticPHPDriver([]); + $class = new ClassMetadata(IncorrectUniqueConstraintByFieldEntity::class); + + $class->initializeReflection(new RuntimeReflectionService()); + $mappingDriver->loadMetadataForClass(IncorrectUniqueConstraintByFieldEntity::class, $class); + + $this->expectException(MappingException::class); + $schemaTool->getSchemaFromMetadata([$class]); + } } /** @@ -425,3 +476,121 @@ class GH6830Category */ public $boards; } + +/** + * @Entity + * @Table( + * name="field_index", + * indexes={ + * @Index(name="index", fields={"index", "fieldName"}), + * }, + * uniqueConstraints={ + * @UniqueConstraint(name="uniq", fields={"index", "table"}) + * } + * ) + */ +class IndexByFieldEntity +{ + /** + * @var int + * @Id + * @Column(type="integer") + */ + public $id; + + /** + * @var string + * @Column + */ + public $index; + + /** + * @var string + * @Column + */ + public $table; + + /** + * @var string + * @Column + */ + public $fieldName; +} + +class IncorrectIndexByFieldEntity +{ + /** @var int */ + public $id; + + /** @var string */ + public $index; + + /** @var string */ + public $table; + + /** @var string */ + public $fieldName; + + public static function loadMetadata(ClassMetadataInfo $metadata): void + { + $metadata->mapField( + [ + 'id' => true, + 'fieldName' => 'id', + ] + ); + + $metadata->mapField(['fieldName' => 'index']); + + $metadata->mapField(['fieldName' => 'table']); + + $metadata->mapField(['fieldName' => 'fieldName']); + + $metadata->setPrimaryTable( + [ + 'indexes' => [ + ['columns' => ['index'], 'fields' => ['fieldName']], + ], + ] + ); + } +} + +class IncorrectUniqueConstraintByFieldEntity +{ + /** @var int */ + public $id; + + /** @var string */ + public $index; + + /** @var string */ + public $table; + + /** @var string */ + public $fieldName; + + public static function loadMetadata(ClassMetadataInfo $metadata): void + { + $metadata->mapField( + [ + 'id' => true, + 'fieldName' => 'id', + ] + ); + + $metadata->mapField(['fieldName' => 'index']); + + $metadata->mapField(['fieldName' => 'table']); + + $metadata->mapField(['fieldName' => 'fieldName']); + + $metadata->setPrimaryTable( + [ + 'uniqueConstraints' => [ + ['columns' => ['index'], 'fields' => ['fieldName']], + ], + ] + ); + } +}