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']],
+ ],
+ ]
+ );
+ }
+}