Schema comparison emits a spurious `DROP INDEX` for the index enforcing a unique constraint
Every supported database platform implements a unique constraint with an index. Introspection reports that index as an ordinary Index. The comparator diffs indexes but does not diff unique constraints, so it treats the enforcing index as an independent object: the declared side has the constraint but no standalone index, the introspected side has the index unmatched against any constraint, and the index looks like a stray to drop.
Consequences
The wrong diff is the same everywhere. What happens when it runs depends on whether the engine guards the index it created.
The diff is wrong — on every platform that reports the backing index (all but SQLite). The comparator reports a difference where the declared schema and the database already agree. This alone is a defect, independent of what follows.
A correctness issue where the database guards its implementation — PostgreSQL, SQL Server, Oracle, Db2. These engines treat the enforcing index as an implementation detail of the constraint and refuse to drop it directly.
Platform Error PostgreSQL cannot drop index uc_duality_email because constraint uc_duality_email on table uc_duality requires itSQL Server An explicit DROP INDEX is not allowed on index 'uc_duality.uc_duality_email'. It is being used for UNIQUE KEY constraint enforcement.Oracle ORA-02429: cannot drop index used for enforcement of unique/primary keyDb2 SQL0669N A system required index cannot be dropped explicitly.Data corruption where the database does not — MySQL, MariaDB. These engines do not guard the enforcing index: a unique constraint is a unique index — per the
CREATE INDEXdocumentation, "AUNIQUEindex creates a constraint such that all values in the index must be distinct" — and dropping the index is permitted. TheDROP INDEXsucceeds and removes the constraint.
Reproduce
$table = Table::editor()
->setUnquotedName('uc_duality')
->setColumns(
Column::editor()
->setUnquotedName('id')
->setTypeName(Types::INTEGER)
->create(),
Column::editor()
->setUnquotedName('email')
->setTypeName(Types::STRING)
->setLength(64)
->create(),
)
->setUniqueConstraints(
UniqueConstraint::editor()
->setUnquotedName('uc_duality_email')
->setUnquotedColumnNames('email')
->create(),
)
->create();
$schemaManager = $connection->createSchemaManager();
$schemaManager->createTable($table);
$diff = $schemaManager->createComparator()
->compareTables($schemaManager->introspectTableByUnquotedName('uc_duality'), $table);
echo implode("\n", $connection->getDatabasePlatform()->getAlterTableSQL($diff));The diff is not empty. On every affected platform it emits a drop of the constraint's index; on MySQL:
DROP INDEX `uc_duality_email` ON uc_dualityOn MySQL, applying it silently voids the constraint — afterward, duplicates insert without error.
Source: doctrine/dbal