diff --git a/src/Mapping/ClassMetadataFactory.php b/src/Mapping/ClassMetadataFactory.php index 8ac89471359..cc37e618a80 100644 --- a/src/Mapping/ClassMetadataFactory.php +++ b/src/Mapping/ClassMetadataFactory.php @@ -34,6 +34,7 @@ use function explode; use function in_array; use function is_a; +use function is_int; use function is_subclass_of; use function method_exists; use function str_contains; @@ -523,6 +524,17 @@ private function addInheritedIndexes(ClassMetadata $subClass, ClassMetadata $par foreach (['uniqueConstraints', 'indexes'] as $indexType) { if (isset($parentClass->table[$indexType])) { foreach ($parentClass->table[$indexType] as $indexName => $index) { + if (is_int($indexName)) { + // Unnamed indices only have a positional key, so identify them by + // their columns: the inheriting table overrides such an index by + // declaring one on the same columns + if (! self::hasIndexOnSameColumns($subClass->table[$indexType] ?? [], $index)) { + $subClass->table[$indexType][] = $index; + } + + continue; + } + if (isset($subClass->table[$indexType][$indexName])) { continue; // Let the inheriting table override indices } @@ -533,6 +545,24 @@ private function addInheritedIndexes(ClassMetadata $subClass, ClassMetadata $par } } + /** + * @param array> $indexes + * @param array $index + */ + private static function hasIndexOnSameColumns(array $indexes, array $index): bool + { + foreach ($indexes as $existingIndex) { + if ( + ($existingIndex['columns'] ?? null) === ($index['columns'] ?? null) + && ($existingIndex['fields'] ?? null) === ($index['fields'] ?? null) + ) { + return true; + } + } + + return false; + } + /** * Completes the ID generator mapping. If "auto" is specified we choose the generator * most appropriate for the targeted database platform. diff --git a/tests/Tests/ORM/Mapping/BasicInheritanceMappingTest.php b/tests/Tests/ORM/Mapping/BasicInheritanceMappingTest.php index 097fb642428..efbbf5a92ab 100644 --- a/tests/Tests/ORM/Mapping/BasicInheritanceMappingTest.php +++ b/tests/Tests/ORM/Mapping/BasicInheritanceMappingTest.php @@ -187,6 +187,61 @@ public function testMappedSuperclassIndex(): void self::assertArrayHasKey('IDX_MAPPED2_INDEX', $class->table['indexes']); } + /** + * Ensure unnamed indexes are inherited from the mapped superclass + * even when the entity declares unnamed indexes of its own. + */ + #[Group('GH-12179')] + public function testMappedSuperclassUnnamedIndexes(): void + { + $class = $this->cmf->getMetadataFor(EntityUnnamedIndexSubClass::class); + assert($class instanceof ClassMetadata); + + self::assertSame( + [ + ['columns' => ['name']], + ['columns' => ['mapped1']], + ], + $class->table['uniqueConstraints'], + ); + + // The indexes on mapped3 and mapped4 are declared by both classes: they must not be duplicated + self::assertSame( + [ + ['columns' => ['code']], + ['columns' => ['mapped3']], + ['fields' => ['mapped4']], + ['columns' => ['mapped2']], + ], + $class->table['indexes'], + ); + } + + /** + * Ensure a named index of the entity replaces an unnamed index of the + * mapped superclass on the same columns instead of duplicating it. + */ + #[Group('GH-12179')] + public function testMappedSuperclassUnnamedIndexOverriddenByNamedIndex(): void + { + $class = $this->cmf->getMetadataFor(EntityNamedIndexSubClass::class); + assert($class instanceof ClassMetadata); + + self::assertSame( + [['columns' => ['mapped1']]], + $class->table['uniqueConstraints'], + ); + + self::assertSame( + [ + 'IDX_MAPPED2' => ['columns' => ['mapped2']], + 0 => ['columns' => ['mapped3']], + 1 => ['fields' => ['mapped4']], + ], + $class->table['indexes'], + ); + } + #[DataProvider('invalidHierarchyDeclarationClasses')] public function testUndeclaredHierarchyRejection(string $rootEntity, string $childClass): void { @@ -318,6 +373,53 @@ class EntityIndexSubClass extends MappedSuperclassBaseIndex private string $name; } +#[Table] +#[UniqueConstraint(columns: ['mapped1'])] +#[Index(columns: ['mapped2'])] +#[Index(columns: ['mapped3'])] +#[Index(fields: ['mapped4'])] +#[MappedSuperclass] +class MappedSuperclassBaseUnnamedIndex +{ + #[Column(type: 'string', length: 255)] + private string $mapped1; + #[Column(type: 'string', length: 255)] + private string $mapped2; + #[Column(type: 'string', length: 255)] + private string $mapped3; + #[Column(type: 'string', length: 255)] + private string $mapped4; +} + +#[Table] +#[UniqueConstraint(columns: ['name'])] +#[Index(columns: ['code'])] +#[Index(columns: ['mapped3'])] +#[Index(fields: ['mapped4'])] +#[Entity] +class EntityUnnamedIndexSubClass extends MappedSuperclassBaseUnnamedIndex +{ + #[Id] + #[Column(type: 'integer')] + private int $id; + + #[Column(type: 'string', length: 255)] + private string $name; + + #[Column(type: 'string', length: 255)] + private string $code; +} + +#[Table] +#[Index(name: 'IDX_MAPPED2', columns: ['mapped2'])] +#[Entity] +class EntityNamedIndexSubClass extends MappedSuperclassBaseUnnamedIndex +{ + #[Id] + #[Column(type: 'integer')] + private int $id; +} + #[Entity] #[InheritanceType('SINGLE_TABLE')] #[DiscriminatorColumn(name: 'type', type: 'string', length: 20)]