diff --git a/src/Migration/IdToUuidMigration.php b/src/Migration/IdToUuidMigration.php index 1a1afaf5..74e932bb 100644 --- a/src/Migration/IdToUuidMigration.php +++ b/src/Migration/IdToUuidMigration.php @@ -14,6 +14,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Schema\AbstractSchemaManager; use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Types\Types; use Psr\Log\LoggerAwareInterface; @@ -26,19 +27,26 @@ * @psalm-type ForeignKey = array{ * table: string, * key: string, - * tmpKey: string, + * uuid_key: string, * nullable: bool, * name: string, * primaryKey: Column[], * onDelete?: string * } + * @psalm-type Index = array{ + * table: string, + * name: string, + * columns: string[], + * primary: bool, + * unique: bool + * } */ final class IdToUuidMigration implements LoggerAwareInterface { - public const UUID_FIELD = 'uuid'; - - public const UUID_TYPE = Types::STRING; + use LogAwareMigration; + public const UUID_FIELD = 'uuid'; + public const UUID_TYPE = Types::STRING; public const UUID_LENGTH = 36; /** @@ -53,6 +61,13 @@ final class IdToUuidMigration implements LoggerAwareInterface */ private array $foreignKeys = []; + /** + * @var array> + * + * @psalm-var array + */ + private array $indexes = []; + private string $idField; private string $table; @@ -61,8 +76,6 @@ final class IdToUuidMigration implements LoggerAwareInterface private AbstractSchemaManager $schemaManager; - private LoggerInterface $logger; - public function __construct(Connection $connection, ?LoggerInterface $logger = null) { $this->connection = $connection; @@ -72,11 +85,6 @@ public function __construct(Connection $connection, ?LoggerInterface $logger = n $this->logger = $logger ?? new NullLogger(); } - public function setLogger(LoggerInterface $logger): void - { - $this->logger = $logger; - } - /** * @param null|callable(mixed $id, string $uuid): void $callback */ @@ -84,12 +92,17 @@ public function migrate(string $tableName, string $idField = 'id', callable $cal { $this->section(sprintf('Migrating %s.%s field to UUID', $tableName, $idField)); - $this->prepare($tableName, $idField); + $this->table = $tableName; + $this->idField = $idField; + + $this->findForeignKeys(); + $this->findIndexes(); $this->addUuidFields(); $this->generateUuidsToReplaceIds(); $this->addThoseUuidsToTablesWithFK(); $this->handleCallback($callback); $this->deletePreviousFKs(); + $this->deleteIndexes(); $this->deletePrimaryKeys(); $this->recreateIdFields(); $this->syncIdFields(); @@ -98,13 +111,12 @@ public function migrate(string $tableName, string $idField = 'id', callable $cal $this->restoreConstraintsAndIndexes(); } - private function prepare(string $tableName, string $idField): void + /** + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + private function findForeignKeys(): void { - $this->table = $tableName; - $this->idField = $idField; - $this->foreignKeys = []; - $this->idToUuidMap = []; foreach ($this->schemaManager->listTables() as $table) { $foreignKeys = $this->schemaManager->listTableForeignKeys($table->getName()); @@ -117,12 +129,12 @@ private function prepare(string $tableName, string $idField): void } $meta = [ - 'table' => $table->getName(), - 'key' => $key, - 'tmpKey' => $key.'_to_uuid', - 'nullable' => $this->isForeignKeyNullable($table, $key), - 'name' => $foreignKey->getName(), - 'primaryKey' => $table->getPrimaryKeyColumns(), + 'table' => $table->getName(), + 'key' => $key, + 'uuid_key' => $key.'_to_uuid', + 'nullable' => $this->isForeignKeyNullable($table, $key), + 'name' => $foreignKey->getName(), + 'primaryKey' => $table->getPrimaryKeyColumns(), ]; $onDelete = $foreignKey->onDelete(); @@ -134,17 +146,68 @@ private function prepare(string $tableName, string $idField): void } } - if (\count($this->foreignKeys) > 0) { - $this->section('Detected foreign keys:'); + if (0 === \count($this->foreignKeys)) { + $this->info('No foreign keys detected'); - foreach ($this->foreignKeys as $meta) { - $this->section(' * '.$meta['table'].'.'.$meta['key']); + return; + } + + $this->section('Detected foreign keys:'); + + foreach ($this->foreignKeys as $meta) { + $this->section(' * '.$meta['table'].'.'.$meta['key']); + } + } + + private function findIndexes(): void + { + $this->indexes = []; + + foreach ($this->schemaManager->listTables() as $table) { + foreach ($table->getIndexes() as $index) { + if (!$this->hasForeignKeyColumns($table->getName(), $index->getColumns())) { + continue; + } + + $this->indexes[] = [ + 'table' => $table->getName(), + 'name' => $index->getName(), + 'columns' => $index->getColumns(), + 'primary' => $index->isPrimary(), + 'unique' => $index->isUnique(), + ]; } + } + + if (0 === \count($this->foreignKeys)) { + $this->info('No indexes detected'); return; } - $this->info('No foreign keys detected'); + $this->section('Detected indexes:'); + + foreach ($this->indexes as $meta) { + $this->section(' * '.$meta['table'].'.'.implode('|', $meta['columns'])); + } + } + + /** + * @param string[] $columns + */ + private function hasForeignKeyColumns(string $table, array $columns): bool + { + foreach ($this->foreignKeys as $foreignKey) { + if ($foreignKey['table'] !== $table) { + continue; + } + + if (\in_array($foreignKey['key'], $columns, true)) { + return true; + } + } + + return false; } private function addUuidFields(): void @@ -161,18 +224,20 @@ private function addUuidFields(): void foreach ($this->foreignKeys as $foreignKey) { $table = $schema->getTable($foreignKey['table']); - $table->addColumn($foreignKey['tmpKey'], self::UUID_TYPE, [ + $table->addColumn($foreignKey['uuid_key'], self::UUID_TYPE, [ 'length' => self::UUID_LENGTH, 'notnull' => false, 'customSchemaOptions' => ['FIRST'], ]); } - $this->schemaManager->migrateSchema($schema); + $this->updateSchema($schema); } private function generateUuidsToReplaceIds(): void { + $this->idToUuidMap = []; + $fetchs = $this->connection->fetchAllAssociative(sprintf('SELECT %s from %s', $this->idField, $this->table)); if (0 === \count($fetchs)) { @@ -228,7 +293,7 @@ private function addThoseUuidsToTablesWithFK(): void } $this->connection->update($foreignKey['table'], [ - $foreignKey['tmpKey'] => $this->idToUuidMap[$fetch[$foreignKey['key']]], + $foreignKey['uuid_key'] => $this->idToUuidMap[$fetch[$foreignKey['key']]], ], $queryPk); } } @@ -250,6 +315,20 @@ private function handleCallback(?callable $callback): void } } + private function deleteIndexes(): void + { + $this->section('Deleting indexes'); + + $schema = $this->schemaManager->createSchema(); + + foreach ($this->indexes as $index) { + $table = $schema->getTable($index['table']); + $table->dropIndex($index['name']); + } + + $this->updateSchema($schema); + } + private function deletePreviousFKs(): void { $this->section('Deleting previous foreign keys'); @@ -258,12 +337,10 @@ private function deletePreviousFKs(): void foreach ($this->foreignKeys as $foreignKey) { $table = $schema->getTable($foreignKey['table']); - $table->removeForeignKey($foreignKey['name']); - $table->dropColumn($foreignKey['key']); } - $this->schemaManager->migrateSchema($schema); + $this->updateSchema($schema); } private function deletePrimaryKeys(): void @@ -275,14 +352,12 @@ private function deletePrimaryKeys(): void foreach ($this->foreignKeys as $foreignKey) { $table = $schema->getTable($foreignKey['table']); - if ([] !== $foreignKey['primaryKey']) { - if ($table->hasPrimaryKey()) { - $table->dropPrimaryKey(); - } + if ($this->hasCombinedPrimaryKey($foreignKey)) { + $table->dropPrimaryKey(); } } - $this->schemaManager->migrateSchema($schema); + $this->updateSchema($schema); } private function recreateIdFields(): void @@ -309,7 +384,7 @@ private function recreateIdFields(): void ]); } - $this->schemaManager->migrateSchema($schema); + $this->updateSchema($schema); } private function syncIdFields(): void @@ -319,7 +394,7 @@ private function syncIdFields(): void $this->connection->executeQuery(sprintf('UPDATE %s SET %s = %s', $this->table, $this->idField, self::UUID_FIELD)); foreach ($this->foreignKeys as $foreignKey) { - $this->connection->executeQuery(sprintf('UPDATE %s SET %s = %s', $foreignKey['table'], $foreignKey['key'], $foreignKey['tmpKey'])); + $this->connection->executeQuery(sprintf('UPDATE %s SET %s = %s', $foreignKey['table'], $foreignKey['key'], $foreignKey['uuid_key'])); } } @@ -332,10 +407,10 @@ private function dropTemporyForeignKeyUuidFields(): void foreach ($this->foreignKeys as $foreignKey) { $table = $schema->getTable($foreignKey['table']); - $table->dropColumn($foreignKey['tmpKey']); + $table->dropColumn($foreignKey['uuid_key']); } - $this->schemaManager->migrateSchema($schema); + $this->updateSchema($schema); } private function dropIdPrimaryKeyAndSetUuidToPrimaryKey(): void @@ -350,11 +425,13 @@ private function dropIdPrimaryKeyAndSetUuidToPrimaryKey(): void $table->setPrimaryKey([$this->idField]); - $this->schemaManager->migrateSchema($schema); + $this->updateSchema($schema); } private function restoreConstraintsAndIndexes(): void { + $this->section('Restore constraints and indexes'); + $schema = $this->schemaManager->createSchema(); foreach ($this->foreignKeys as $foreignKey) { @@ -368,35 +445,39 @@ private function restoreConstraintsAndIndexes(): void } } + $table->changeColumn($foreignKey['key'], [ + 'notnull' => !$foreignKey['nullable'], + ]); $table->addForeignKeyConstraint($this->table, [$foreignKey['key']], [$this->idField], [ 'onDelete' => $foreignKey['onDelete'] ?? null, ]); $table->addIndex([$foreignKey['key']]); } - $this->schemaManager->migrateSchema($schema); - } + foreach ($this->indexes as $index) { + $table = $schema->getTable($index['table']); - private function section(string $message): void - { - $this->writeLn(sprintf('%s', $message)); - } + if ($index['unique'] && !$index['primary']) { + $table->addUniqueIndex($index['columns'], $index['name']); + } + } - private function info(string $message): void - { - $this->writeLn(sprintf('-> %s', $message)); + $this->updateSchema($schema); } - private function debug(string $message): void + /** + * @psalm-param ForeignKey $foreignKey + */ + private function hasCombinedPrimaryKey(array $foreignKey): bool { - $this->writeLn(sprintf(' * %s', $message)); - } + /** @var Column $key */ + foreach ($foreignKey['primaryKey'] as $key) { + if ($key->getName() === $foreignKey['key']) { + return true; + } + } - private function writeLn(string $message): void - { - $this->logger->notice($message, [ - 'migration' => $this, - ]); + return false; } private function isForeignKeyNullable(Table $table, string $key): bool @@ -409,4 +490,9 @@ private function isForeignKeyNullable(Table $table, string $key): bool throw new RuntimeException('Unable to find '.$key.'in '.$table->getName()); } + + private function updateSchema(Schema $schema): void + { + $this->schemaManager->migrateSchema($schema); + } } diff --git a/src/Migration/LogAwareMigration.php b/src/Migration/LogAwareMigration.php new file mode 100644 index 00000000..f602e5d5 --- /dev/null +++ b/src/Migration/LogAwareMigration.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Nucleos\Doctrine\Migration; + +use Psr\Log\LoggerAwareTrait; + +trait LogAwareMigration +{ + use LoggerAwareTrait; + + protected function section(string $message): void + { + $this->writeLn(sprintf('%s', $message)); + } + + protected function info(string $message): void + { + $this->writeLn(sprintf('-> %s', $message)); + } + + protected function debug(string $message): void + { + $this->writeLn(sprintf(' * %s', $message)); + } + + protected function writeLn(string $message): void + { + $this->logger?->notice($message, [ + 'migration' => $this, + ]); + } +} diff --git a/tests/Migration/IdToUuidMigrationTest.php b/tests/Migration/IdToUuidMigrationTest.php index 987ba03d..cbc8d23a 100644 --- a/tests/Migration/IdToUuidMigrationTest.php +++ b/tests/Migration/IdToUuidMigrationTest.php @@ -50,6 +50,8 @@ private function createCategory(Schema $schema): void $table->addForeignKeyConstraint('category', ['parent_id'], ['id'], [ 'onDelete' => 'SET NULL', ]); + $table->addUniqueIndex(['parent_id', 'name']); + $table->addIndex(['name']); $table->addIndex(['parent_id']); $table->setPrimaryKey(['id']); } @@ -65,7 +67,7 @@ private function createItem(Schema $schema): void 'length' => 50, ]); $table->addForeignKeyConstraint('category', ['category_id'], ['id'], [ - 'onDelete' => 'SET NULL', + 'onDelete' => 'CASCADE', ]); $table->addIndex(['category_id']); $table->setPrimaryKey(['id']); @@ -89,6 +91,7 @@ private function createSchema(Connection $connection): void $schemaManager = method_exists($connection, 'createSchemaManager') ? $connection->createSchemaManager() : $connection->getSchemaManager(); + $schema = $schemaManager->createSchema(); $this->createCategory($schema); $this->createItem($schema);