diff --git a/docs/en/index.rst b/docs/en/index.rst index 26b41793..f6103255 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -550,6 +550,21 @@ You can also output the results as a JSON formatted string using the You can also use the ``--source``, ``--connection`` and ``--plugin`` options just like for the ``migrate`` command. +Cleaning up missing migrations +------------------------------- + +Sometimes migration files may be deleted from the filesystem but still exist +in the phinxlog table. These migrations will be marked as **MISSING** in the +status output. You can remove these entries from the phinxlog table using the +``--cleanup`` option: + +.. code-block:: bash + + bin/cake migrations status --cleanup + +This will remove all migration entries from the phinxlog table that no longer +have corresponding migration files in the filesystem. + Marking a migration as migrated =============================== diff --git a/src/Command/StatusCommand.php b/src/Command/StatusCommand.php index 442a0e2f..1c773436 100644 --- a/src/Command/StatusCommand.php +++ b/src/Command/StatusCommand.php @@ -63,6 +63,8 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar '', 'migrations status -c secondary', 'migrations status -c secondary -f json', + 'migrations status --cleanup', + 'Remove *MISSING* migrations from the phinxlog table', ])->addOption('plugin', [ 'short' => 'p', 'help' => 'The plugin to run migrations for', @@ -79,6 +81,10 @@ public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionPar 'help' => 'The output format: text or json. Defaults to text.', 'choices' => ['text', 'json'], 'default' => 'text', + ])->addOption('cleanup', [ + 'help' => 'Remove MISSING migrations from the phinxlog table', + 'boolean' => true, + 'default' => false, ]); return $parser; @@ -95,6 +101,7 @@ public function execute(Arguments $args, ConsoleIo $io): ?int { /** @var string|null $format */ $format = $args->getOption('format'); + $clean = $args->getOption('cleanup'); $factory = new ManagerFactory([ 'plugin' => $args->getOption('plugin'), @@ -103,6 +110,18 @@ public function execute(Arguments $args, ConsoleIo $io): ?int 'dry-run' => $args->getOption('dry-run'), ]); $manager = $factory->createManager($io); + + if ($clean) { + $removed = $manager->cleanupMissingMigrations(); + if ($removed === 0) { + $io->out('No missing migrations to clean up.'); + } else { + $io->out(sprintf('Removed %d missing migration(s) from migration log.', $removed)); + } + + return Command::CODE_SUCCESS; + } + $migrations = $manager->printStatus($format); switch ($format) { diff --git a/src/Migration/Manager.php b/src/Migration/Manager.php index 37bdc9b4..226bfb23 100644 --- a/src/Migration/Manager.php +++ b/src/Migration/Manager.php @@ -1183,4 +1183,47 @@ public function resetSeeds(): void { $this->seeds = null; } + + /** + * Cleanup missing migrations from the phinxlog table + * + * Removes entries from the phinxlog table for migrations that no longer exist + * in the migrations directory (marked as MISSING in status output). + * + * @return int The number of missing migrations removed + */ + public function cleanupMissingMigrations(): int + { + $defaultMigrations = $this->getMigrations(); + $env = $this->getEnvironment(); + $versions = $env->getVersionLog(); + $adapter = $env->getAdapter(); + + // Find missing migrations (those in phinxlog but not in filesystem) + $missingVersions = []; + foreach ($versions as $versionId => $versionInfo) { + if (!isset($defaultMigrations[$versionId])) { + $missingVersions[] = $versionId; + } + } + + if (!$missingVersions) { + return 0; + } + + // Remove missing migrations from phinxlog + $adapter->beginTransaction(); + try { + $delete = $adapter->getDeleteBuilder() + ->from($env->getSchemaTableName()) + ->where(['version IN' => $missingVersions]); + $delete->execute(); + $adapter->commitTransaction(); + } catch (Exception $e) { + $adapter->rollbackTransaction(); + throw $e; + } + + return count($missingVersions); + } } diff --git a/tests/TestCase/Command/CompletionTest.php b/tests/TestCase/Command/CompletionTest.php index 46d8405d..b30b2304 100644 --- a/tests/TestCase/Command/CompletionTest.php +++ b/tests/TestCase/Command/CompletionTest.php @@ -124,7 +124,7 @@ public function testMigrationsOptionsStatus() $this->exec('completion options migrations.migrations status'); $this->assertCount(1, $this->_out->messages()); $output = $this->_out->messages()[0]; - $expected = '--connection -c --format -f --help -h --plugin -p --quiet -q --source -s --verbose -v'; + $expected = '--cleanup --connection -c --format -f --help -h --plugin -p --quiet -q --source -s --verbose -v'; $outputExplode = explode(' ', trim($output)); sort($outputExplode); $expectedExplode = explode(' ', $expected); diff --git a/tests/TestCase/Command/StatusCommandTest.php b/tests/TestCase/Command/StatusCommandTest.php index d9562b73..e1ea9102 100644 --- a/tests/TestCase/Command/StatusCommandTest.php +++ b/tests/TestCase/Command/StatusCommandTest.php @@ -78,4 +78,46 @@ public function testExecuteConnectionDoesNotExist(): void $this->expectException(RuntimeException::class); $this->exec('migrations status -c lolnope'); } + + public function testCleanNoMissingMigrations(): void + { + $this->exec('migrations status -c test --cleanup'); + $this->assertExitSuccess(); + $this->assertOutputContains('No missing migrations to clean up.'); + } + + public function testCleanWithMissingMigrations(): void + { + // First, insert a fake migration entry that doesn't exist in filesystem + $table = $this->fetchTable('Phinxlog'); + $entity = $table->newEntity([ + 'version' => 99999999999999, + 'migration_name' => 'FakeMissingMigration', + 'start_time' => '2024-01-01 00:00:00', + 'end_time' => '2024-01-01 00:00:01', + 'breakpoint' => false, + ]); + $table->save($entity); + + // Verify the fake migration is in the table + $count = $table->find()->where(['version' => 99999999999999])->count(); + $this->assertEquals(1, $count); + + // Run the clean command + $this->exec('migrations status -c test --cleanup'); + $this->assertExitSuccess(); + $this->assertOutputContains('Removed 1 missing migration(s) from migration log.'); + + // Verify the fake migration was removed + $count = $table->find()->where(['version' => 99999999999999])->count(); + $this->assertEquals(0, $count); + } + + public function testCleanHelp(): void + { + $this->exec('migrations status --help'); + $this->assertExitSuccess(); + $this->assertOutputContains('--cleanup'); + $this->assertOutputContains('Remove MISSING migrations from the phinxlog table'); + } }