diff --git a/lib/Doctrine/Migrations/Exception/MetadataStorageError.php b/lib/Doctrine/Migrations/Exception/MetadataStorageError.php index 5fd680d4a..e43432f87 100644 --- a/lib/Doctrine/Migrations/Exception/MetadataStorageError.php +++ b/lib/Doctrine/Migrations/Exception/MetadataStorageError.php @@ -8,6 +8,11 @@ final class MetadataStorageError extends RuntimeException implements MigrationException { + public static function errorSavingMetadata(): self + { + return new self('The metadata storage is not up to date, please run the sync-metadata-storage command to fix this issue.'); + } + public static function notUpToDate(): self { return new self('The metadata storage is not up to date, please run the sync-metadata-storage command to fix this issue.'); diff --git a/lib/Doctrine/Migrations/Metadata/Storage/TableMetadataStorage.php b/lib/Doctrine/Migrations/Metadata/Storage/TableMetadataStorage.php index 729527067..42ea39f6f 100644 --- a/lib/Doctrine/Migrations/Metadata/Storage/TableMetadataStorage.php +++ b/lib/Doctrine/Migrations/Metadata/Storage/TableMetadataStorage.php @@ -37,6 +37,10 @@ final class TableMetadataStorage implements MetadataStorage { + private const RUN_EXECUTED = 'execute'; + private const RUN_SKIPPED = 'skip'; + private const RUN_ERRORED = 'error'; + /** @var bool */ private $isInitialized; @@ -87,7 +91,15 @@ public function getExecutedMigrations(): ExecutedMigrationsList } $this->checkInitialization(); - $rows = $this->connection->fetchAllAssociative(sprintf('SELECT * FROM %s', $this->configuration->getTableName())); + $rows = $this->connection->fetchAllAssociative( + sprintf( + 'SELECT * FROM %s WHERE %s = :reason', + $this->configuration->getTableName(), + $this->configuration->getExecutionReasonColumnName() + ), + ['reason' => self::RUN_EXECUTED], + ['reason' => Types::STRING] + ); $migrations = []; foreach ($rows as $row) { @@ -136,6 +148,10 @@ public function complete(ExecutionResult $result): void { $this->checkInitialization(); + $reason = ($result->isSkipped() + ? 'skipp' + : ($result->hasError() ? 'error' : 'execute')); + if ($result->getDirection() === Direction::DOWN) { $this->connection->delete($this->configuration->getTableName(), [ $this->configuration->getVersionColumnName() => (string) $result->getVersion(), @@ -145,10 +161,12 @@ public function complete(ExecutionResult $result): void $this->configuration->getVersionColumnName() => (string) $result->getVersion(), $this->configuration->getExecutedAtColumnName() => $result->getExecutedAt(), $this->configuration->getExecutionTimeColumnName() => $result->getTime() === null ? null : (int) round($result->getTime() * 1000), + $this->configuration->getExecutionReasonColumnName() => $reason, ], [ Types::STRING, Types::DATETIME_MUTABLE, Types::INTEGER, + Types::STRING, ]); } } @@ -228,6 +246,11 @@ private function getExpectedTable(): Table ); $schemaChangelog->addColumn($this->configuration->getExecutedAtColumnName(), 'datetime', ['notnull' => false]); $schemaChangelog->addColumn($this->configuration->getExecutionTimeColumnName(), 'integer', ['notnull' => false]); + $schemaChangelog->addColumn( + $this->configuration->getExecutionReasonColumnName(), + 'string', + ['notnull' => true, 'default' => self::RUN_EXECUTED] + ); $schemaChangelog->setPrimaryKey([$this->configuration->getVersionColumnName()]); diff --git a/lib/Doctrine/Migrations/Metadata/Storage/TableMetadataStorageConfiguration.php b/lib/Doctrine/Migrations/Metadata/Storage/TableMetadataStorageConfiguration.php index 655f4357e..9bc01524d 100644 --- a/lib/Doctrine/Migrations/Metadata/Storage/TableMetadataStorageConfiguration.php +++ b/lib/Doctrine/Migrations/Metadata/Storage/TableMetadataStorageConfiguration.php @@ -21,6 +21,9 @@ final class TableMetadataStorageConfiguration implements MetadataStorageConfigur /** @var string */ private $executionTimeColumnName = 'execution_time'; + /** @var string */ + private $executionReasonColumnName = 'reason'; + public function getTableName(): string { return $this->tableName; @@ -66,6 +69,16 @@ public function getExecutionTimeColumnName(): string return $this->executionTimeColumnName; } + public function getExecutionReasonColumnName(): string + { + return $this->executionReasonColumnName; + } + + public function setExecutionReasonColumnName(string $executionReasonColumnName): void + { + $this->executionReasonColumnName = $executionReasonColumnName; + } + public function setExecutionTimeColumnName(string $executionTimeColumnName): void { $this->executionTimeColumnName = $executionTimeColumnName; diff --git a/lib/Doctrine/Migrations/Version/DbalExecutor.php b/lib/Doctrine/Migrations/Version/DbalExecutor.php index 92e6e0fe1..cca0a921c 100644 --- a/lib/Doctrine/Migrations/Version/DbalExecutor.php +++ b/lib/Doctrine/Migrations/Version/DbalExecutor.php @@ -10,6 +10,7 @@ use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\EventDispatcher; use Doctrine\Migrations\Events; +use Doctrine\Migrations\Exception\MetadataStorageError; use Doctrine\Migrations\Exception\SkipMigration; use Doctrine\Migrations\Metadata\MigrationPlan; use Doctrine\Migrations\Metadata\Storage\MetadataStorage; @@ -105,6 +106,10 @@ public function execute( ); $result->setSql($this->sql); + } catch (MetadataStorageError $e) { + $result->setError(true, $e); + + $this->migrationEnd($e, $plan, $result, $configuration); } catch (SkipMigration $e) { $result->setSkipped(true); @@ -209,7 +214,11 @@ private function executeMigration( $this->logger->info('Migration {version} {direction} (took {time}ms, used {memory} memory)', $params); if (! $configuration->isDryRun()) { - $this->metadataStorage->complete($result); + try { + $this->metadataStorage->complete($result); + } catch (Throwable $e) { + throw MetadataStorageError::errorSavingMetadata(); + } } if ($migration->isTransactional()) { @@ -251,7 +260,12 @@ private function getMigrationHeader(MigrationPlan $planItem, AbstractMigration $ private function migrationEnd(Throwable $e, MigrationPlan $plan, ExecutionResult $result, MigratorConfiguration $configuration): void { + if (! $configuration->isDryRun() && ! ($e instanceof MetadataStorageError)) { + $this->metadataStorage->complete($result); + } + $migration = $plan->getMigration(); + if ($migration->isTransactional()) { //only rollback transaction if in transactional mode TransactionHelper::rollbackIfInTransaction($this->connection); diff --git a/tests/Doctrine/Migrations/Tests/Metadata/Storage/ExistingTableMetadataStorageTest.php b/tests/Doctrine/Migrations/Tests/Metadata/Storage/ExistingTableMetadataStorageTest.php index 442937cbb..0fdf7e4ed 100644 --- a/tests/Doctrine/Migrations/Tests/Metadata/Storage/ExistingTableMetadataStorageTest.php +++ b/tests/Doctrine/Migrations/Tests/Metadata/Storage/ExistingTableMetadataStorageTest.php @@ -118,11 +118,13 @@ public function testMigratedVersionUpdate(): void 'version' => '1234', 'executed_at' => null, 'execution_time' => null, + 'reason' => 'execute', ], $rows[0]); self::assertSame([ 'version' => 'Foo\\5678', 'executed_at' => null, 'execution_time' => null, + 'reason' => 'execute', ], $rows[1]); } } diff --git a/tests/Doctrine/Migrations/Tests/Metadata/Storage/TableMetadataStorageTest.php b/tests/Doctrine/Migrations/Tests/Metadata/Storage/TableMetadataStorageTest.php index 929f8a619..d889e9ebb 100644 --- a/tests/Doctrine/Migrations/Tests/Metadata/Storage/TableMetadataStorageTest.php +++ b/tests/Doctrine/Migrations/Tests/Metadata/Storage/TableMetadataStorageTest.php @@ -183,6 +183,7 @@ public function testComplete(): void 'version' => '1230', 'executed_at' => '2010-01-05 10:30:21', 'execution_time' => '31000', + 'reason' => 'execute', ], ], $rows); } @@ -206,6 +207,7 @@ public function testCompleteWithFloatTime(): void 'version' => '1230', 'executed_at' => '2010-01-05 10:30:21', 'execution_time' => '31490', + 'reason' => 'execute', ], ], $rows); } @@ -232,11 +234,13 @@ public function testCompleteWillAlwaysCastTimeToInteger(): void $config->getVersionColumnName() => '1230', $config->getExecutedAtColumnName() => $executedAt, $config->getExecutionTimeColumnName() => 31000, + $config->getExecutionReasonColumnName() => 'execute', ], $params); self::assertSame([ Types::STRING, Types::DATETIME_MUTABLE, Types::INTEGER, + Types::STRING, ], $types); return 1; diff --git a/tests/Doctrine/Migrations/Tests/Version/ExecutorTest.php b/tests/Doctrine/Migrations/Tests/Version/ExecutorTest.php index b6715860e..2655fb26c 100644 --- a/tests/Doctrine/Migrations/Tests/Version/ExecutorTest.php +++ b/tests/Doctrine/Migrations/Tests/Version/ExecutorTest.php @@ -106,6 +106,8 @@ public function testExecuteUp(): void ->expects(self::once()) ->method('complete')->willReturnCallback(static function (ExecutionResult $result): void { self::assertSame(Direction::UP, $result->getDirection()); + self::assertFalse($result->isSkipped()); + self::assertFalse($result->hasError()); self::assertNotNull($result->getTime()); self::assertNotNull($result->getExecutedAt()); }); @@ -169,6 +171,8 @@ public function testExecuteDown(): void ->expects(self::once()) ->method('complete')->willReturnCallback(static function (ExecutionResult $result): void { self::assertSame(Direction::DOWN, $result->getDirection()); + self::assertFalse($result->isSkipped()); + self::assertFalse($result->hasError()); self::assertNotNull($result->getTime()); self::assertNotNull($result->getExecutedAt()); }); @@ -266,8 +270,14 @@ public function testExecuteDryRun(): void public function testSkipMigration(): void { $this->metadataStorage - ->expects(self::never()) - ->method('complete'); + ->expects(self::once()) + ->method('complete')->willReturnCallback(static function (ExecutionResult $result): void { + self::assertSame(Direction::UP, $result->getDirection()); + self::assertTrue($result->isSkipped()); + self::assertFalse($result->hasError()); + self::assertNull($result->getTime()); + self::assertNotNull($result->getExecutedAt()); + }); $migratorConfiguration = (new MigratorConfiguration()) ->setTimeAllQueries(true); @@ -363,8 +373,14 @@ public function onMigrationsVersionExecuted(): void public function testErrorMigration(): void { $this->metadataStorage - ->expects(self::never()) - ->method('complete'); + ->expects(self::once()) + ->method('complete')->willReturnCallback(static function (ExecutionResult $result): void { + self::assertSame(Direction::UP, $result->getDirection()); + self::assertFalse($result->isSkipped()); + self::assertTrue($result->hasError()); + self::assertNull($result->getTime()); + self::assertNotNull($result->getExecutedAt()); + }); $migratorConfiguration = (new MigratorConfiguration()) ->setTimeAllQueries(true); @@ -472,30 +488,23 @@ public function onMigrationsVersionSkipped(): void $this->eventManager->addEventListener(Events::onMigrationsVersionExecuted, $listener); $this->eventManager->addEventListener(Events::onMigrationsVersionSkipped, $listener); - $migrationSucceed = false; - try { - $this->versionExecutor->execute( - $plan, - $migratorConfiguration - ); - $migrationSucceed = true; - } catch (Throwable $e) { - self::assertFalse($listener->onMigrationsVersionExecuted); - self::assertTrue($listener->onMigrationsVersionSkipped); - self::assertTrue($listener->onMigrationsVersionExecuting); - - $result = $plan->getResult(); - self::assertNotNull($result); - self::assertSame([], $result->getSql()); - self::assertSame([], $result->getSql()); - self::assertSame(State::POST, $result->getState()); - self::assertTrue($this->migration->preUpExecuted); - self::assertTrue($this->migration->postUpExecuted); - self::assertFalse($this->migration->preDownExecuted); - self::assertFalse($this->migration->postDownExecuted); - } + $this->versionExecutor->execute( + $plan, + $migratorConfiguration + ); + self::assertFalse($listener->onMigrationsVersionExecuted); + self::assertTrue($listener->onMigrationsVersionSkipped); + self::assertTrue($listener->onMigrationsVersionExecuting); - self::assertFalse($migrationSucceed); + $result = $plan->getResult(); + self::assertNotNull($result); + self::assertSame([], $result->getSql()); + self::assertSame([], $result->getSql()); + self::assertSame(State::POST, $result->getState()); + self::assertTrue($this->migration->preUpExecuted); + self::assertTrue($this->migration->postUpExecuted); + self::assertFalse($this->migration->preDownExecuted); + self::assertFalse($this->migration->postDownExecuted); } /**