diff --git a/src/Command/DownCommand.php b/src/Command/DownCommand.php index 900be35f..0843706e 100644 --- a/src/Command/DownCommand.php +++ b/src/Command/DownCommand.php @@ -12,6 +12,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; +use Throwable; use Yiisoft\Db\Migration\Informer\ConsoleMigrationInformer; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Runner\DownRunner; @@ -90,8 +91,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int "Total $n $migrationWord to be reverted:\n" ); - foreach ($migrations as $migration) { - $output->writeln("\t$migration"); + foreach ($migrations as $i => $migration) { + $output->writeln("\t" . ($i + 1) . ". $migration"); } /** @var QuestionHelper $helper */ @@ -103,15 +104,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int ); if ($helper->ask($input, $output, $question)) { - /** @psalm-var class-string[] $migrations */ $instances = $this->migrationService->makeRevertibleMigrations($migrations); - foreach ($instances as $instance) { - $this->downRunner->run($instance); + $migrationWas = ($n === 1 ? 'migration was' : 'migrations were'); + + foreach ($instances as $i => $instance) { + try { + $this->downRunner->run($instance); + } catch (Throwable $e) { + $output->writeln("\n\n\t>>> [ERROR] - Not reverted " . $instance::class . ''); + $output->writeln("\n >>> Total $i out of $n $migrationWas reverted.\n"); + $io->error($i > 0 ? 'Partially reverted.' : 'Not reverted.'); + + throw $e; + } } - $output->writeln( - "\n >>> [OK] $n " . ($n === 1 ? 'migration was' : 'migrations were') . " reverted.\n" - ); + $output->writeln("\n >>> [OK] $n $migrationWas reverted.\n"); $io->success('Migrated down successfully.'); } diff --git a/src/Command/NewCommand.php b/src/Command/NewCommand.php index 8b736cb9..e2889a62 100644 --- a/src/Command/NewCommand.php +++ b/src/Command/NewCommand.php @@ -79,7 +79,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::INVALID; } - /** @psalm-var class-string[] $migrations */ $migrations = $this->migrationService->getNewMigrations(); if (empty($migrations)) { @@ -100,8 +99,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->section("Found $n new $migrationWord:"); } - foreach ($migrations as $migration) { - $output->writeln("\t{$migration}"); + foreach ($migrations as $i => $migration) { + $output->writeln("\t" . ($i + 1) . ". $migration"); } $this->migrationService->databaseConnection(); diff --git a/src/Command/RedoCommand.php b/src/Command/RedoCommand.php index eaf680f3..94502ab7 100644 --- a/src/Command/RedoCommand.php +++ b/src/Command/RedoCommand.php @@ -12,6 +12,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; +use Throwable; use Yiisoft\Db\Migration\Informer\ConsoleMigrationInformer; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Runner\DownRunner; @@ -89,32 +90,50 @@ protected function execute(InputInterface $input, OutputInterface $output): int $migrations = array_keys($migrations); $n = count($migrations); - $output->writeln("Total $n " . ($n === 1 ? 'migration' : 'migrations') . " to be redone:\n"); + $migrationWord = $n === 1 ? 'migration' : 'migrations'; - foreach ($migrations as $migration) { - $output->writeln("\t$migration"); + $output->writeln("Total $n $migrationWord to be redone:\n"); + + foreach ($migrations as $i => $migration) { + $output->writeln("\t" . ($i + 1) . ". $migration"); } /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $question = new ConfirmationQuestion( - "\nRedo the above " . ($n === 1 ? 'migration y/n: ' : 'migrations y/n: '), + "\nRedo the above $migrationWord y/n: ", true ); if ($helper->ask($input, $output, $question)) { - /** @psalm-var class-string[] $migrations */ $instances = $this->migrationService->makeRevertibleMigrations($migrations); - foreach ($instances as $instance) { - $this->downRunner->run($instance); + $migrationWas = ($n === 1 ? 'migration was' : 'migrations were'); + + foreach ($instances as $i => $instance) { + try { + $this->downRunner->run($instance); + } catch (Throwable $e) { + $output->writeln("\n\n\t>>> [ERROR] - Not reverted " . $instance::class . ''); + $output->writeln("\n >>> Total $i out of $n $migrationWas reverted.\n"); + $io->error($i > 0 ? 'Partially reverted.' : 'Not reverted.'); + + throw $e; + } } - foreach (array_reverse($instances) as $instance) { - $this->updateRunner->run($instance); + + foreach (array_reverse($instances) as $i => $instance) { + try { + $this->updateRunner->run($instance); + } catch (Throwable $e) { + $output->writeln("\n\n\t>>> [ERROR] - Not applied " . $instance::class . ''); + $output->writeln("\n >>> Total $i out of $n $migrationWas applied.\n"); + $io->error($i > 0 ? 'Reverted but partially applied.' : 'Reverted but not applied.'); + + throw $e; + } } - $output->writeln( - "\n >>> $n " . ($n === 1 ? 'migration was' : 'migrations were') . " redone.\n" - ); + $output->writeln("\n >>> $n $migrationWas redone.\n"); $io->success('Migration redone successfully.'); } diff --git a/src/Command/UpdateCommand.php b/src/Command/UpdateCommand.php index 43ae2174..e7879a3b 100644 --- a/src/Command/UpdateCommand.php +++ b/src/Command/UpdateCommand.php @@ -12,6 +12,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; use Symfony\Component\Console\Style\SymfonyStyle; +use Throwable; use Yiisoft\Db\Migration\Informer\ConsoleMigrationInformer; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Runner\UpdateRunner; @@ -92,7 +93,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - /** @psalm-var class-string[] $migrations */ $migrations = $this->migrationService->getNewMigrations(); if (empty($migrations)) { @@ -114,7 +114,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $output->writeln("Total $n new $migrationWord to be applied:\n"); } - foreach ($migrations as $migration) { + foreach ($migrations as $i => $migration) { $nameLimit = $this->migrator->getMigrationNameLimit(); if (strlen($migration) > $nameLimit) { @@ -126,26 +126,34 @@ protected function execute(InputInterface $input, OutputInterface $output): int return Command::INVALID; } - $output->writeln("\t$migration"); + $output->writeln("\t" . ($i + 1) . ". $migration"); } /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); $question = new ConfirmationQuestion( - "\nApply the above " . ($n === 1 ? 'migration y/n: ' : 'migrations y/n: '), + "\nApply the above $migrationWord y/n: ", true ); if ($helper->ask($input, $output, $question)) { $instances = $this->migrationService->makeMigrations($migrations); - foreach ($instances as $instance) { - $this->updateRunner->run($instance); + $migrationWas = ($n === 1 ? 'migration was' : 'migrations were'); + + foreach ($instances as $i => $instance) { + try { + $this->updateRunner->run($instance); + } catch (Throwable $e) { + $output->writeln("\n\n\t>>> [ERROR] - Not applied " . $instance::class . ''); + $output->writeln("\n >>> Total $i out of $n new $migrationWas applied.\n"); + $io->error($i > 0 ? 'Partially updated.' : 'Not updated.'); + + throw $e; + } } - $output->writeln( - "\n >>> $n " . ($n === 1 ? 'Migration was' : 'Migrations were') . " applied.\n" - ); + $output->writeln("\n >>> Total $n new $migrationWas applied.\n"); $io->success('Updated successfully.'); } diff --git a/src/Migrator.php b/src/Migrator.php index a3e332d5..aee46ff0 100644 --- a/src/Migrator.php +++ b/src/Migrator.php @@ -78,7 +78,7 @@ public function getMigrationNameLimit(): ?int return $this->migrationNameLimit = $limit; } - /** @psalm-return array */ + /** @psalm-return array */ public function getHistory(?int $limit = null): array { $this->checkMigrationHistoryTable(); @@ -93,7 +93,7 @@ public function getHistory(?int $limit = null): array $query->limit($limit); } - /** @psalm-var array */ + /** @psalm-var array */ return $query->column(); } diff --git a/src/Service/MigrationService.php b/src/Service/MigrationService.php index ce912c82..3fb278ca 100644 --- a/src/Service/MigrationService.php +++ b/src/Service/MigrationService.php @@ -106,6 +106,8 @@ public function before(string $defaultName): int * Returns the migrations that are not applied. * * @return array List of new migrations. + * + * @psalm-return array */ public function getNewMigrations(): array { @@ -158,6 +160,7 @@ public function getNewMigrations(): array } ksort($migrations); + /** @psalm-var array */ return array_values($migrations); } diff --git a/tests/Common/Command/AbstractDownCommandTest.php b/tests/Common/Command/AbstractDownCommandTest.php index 8f61a565..6c031282 100644 --- a/tests/Common/Command/AbstractDownCommandTest.php +++ b/tests/Common/Command/AbstractDownCommandTest.php @@ -10,6 +10,7 @@ use RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; +use Throwable; use Yiisoft\Db\Connection\ConnectionInterface; use Yiisoft\Db\Migration\Command\DownCommand; use Yiisoft\Db\Migration\Migrator; @@ -273,6 +274,69 @@ public function testIncorrectLimit(int $limit): void $this->assertStringContainsString('The limit option must be greater than 0.', $output); } + public function testPartiallyReverted(): void + { + MigrationHelper::useMigrationsNamespace($this->container); + MigrationHelper::createAndApplyMigration( + $this->container, + 'Create_Book', + 'table', + 'book', + ['title:string(100)', 'author:string(80)'], + ); + MigrationHelper::createAndApplyMigration( + $this->container, + 'Create_Chapter', + 'table', + 'chapter', + ['name:string(100)'], + ); + + $db = $this->container->get(ConnectionInterface::class); + $db->createCommand()->dropTable('book')->execute(); + + $command = $this->createCommand($this->container); + + try { + $exitCode = $command->setInputs(['yes'])->execute(['-l' => 2]); + } catch (Throwable) { + } + + $output = $command->getDisplay(true); + + $this->assertFalse(isset($exitCode)); + $this->assertStringContainsString('>>> Total 1 out of 2 migrations were reverted.', $output); + $this->assertStringContainsString('[ERROR] Partially reverted.', $output); + } + + public function testNotReverted(): void + { + MigrationHelper::useMigrationsNamespace($this->container); + MigrationHelper::createAndApplyMigration( + $this->container, + 'Create_Book', + 'table', + 'book', + ['title:string(100)', 'author:string(80)'], + ); + + $db = $this->container->get(ConnectionInterface::class); + $db->createCommand()->dropTable('book')->execute(); + + $command = $this->createCommand($this->container); + + try { + $exitCode = $command->setInputs(['yes'])->execute([]); + } catch (Throwable) { + } + + $output = $command->getDisplay(true); + + $this->assertFalse(isset($exitCode)); + $this->assertStringContainsString('>>> Total 0 out of 1 migration was reverted.', $output); + $this->assertStringContainsString('[ERROR] Not reverted.', $output); + } + public function createCommand(ContainerInterface $container): CommandTester { return CommandHelper::getCommandTester($container, DownCommand::class); diff --git a/tests/Common/Command/AbstractRedoCommandTest.php b/tests/Common/Command/AbstractRedoCommandTest.php index 516062d6..47b9063d 100644 --- a/tests/Common/Command/AbstractRedoCommandTest.php +++ b/tests/Common/Command/AbstractRedoCommandTest.php @@ -9,11 +9,15 @@ use RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; +use Throwable; +use Yiisoft\Db\Connection\ConnectionInterface; use Yiisoft\Db\Migration\Command\RedoCommand; use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Tests\Support\AssertTrait; use Yiisoft\Db\Migration\Tests\Support\Helper\CommandHelper; +use Yiisoft\Db\Migration\Tests\Support\Helper\DbHelper; use Yiisoft\Db\Migration\Tests\Support\Helper\MigrationHelper; +use Yiisoft\Db\Migration\Tests\Support\Migrations\M231017150317EmptyDown; use Yiisoft\Db\Migration\Tests\Support\Stub\StubMigration; abstract class AbstractRedoCommandTest extends TestCase @@ -197,6 +201,123 @@ public function testOptionAll(): void $this->assertStringContainsString('Total 2 migrations to be redone:', $output); } + public function testPartiallyReverted(): void + { + MigrationHelper::useMigrationsNamespace($this->container); + MigrationHelper::createAndApplyMigration( + $this->container, + 'Create_Book', + 'table', + 'book', + ['title:string(100)', 'author:string(80)'], + ); + MigrationHelper::createAndApplyMigration( + $this->container, + 'Create_Chapter', + 'table', + 'chapter', + ['name:string(100)'], + ); + + $db = $this->container->get(ConnectionInterface::class); + $db->createCommand()->dropTable('book')->execute(); + + $command = $this->createCommand($this->container); + + try { + $exitCode = $command->setInputs(['yes'])->execute(['-l' => 2]); + } catch (Throwable) { + } + + $output = $command->getDisplay(true); + + $this->assertFalse(isset($exitCode)); + $this->assertStringContainsString('>>> Total 1 out of 2 migrations were reverted.', $output); + $this->assertStringContainsString('[ERROR] Partially reverted.', $output); + } + + public function testNotReverted(): void + { + MigrationHelper::useMigrationsNamespace($this->container); + MigrationHelper::createAndApplyMigration( + $this->container, + 'Create_Book', + 'table', + 'book', + ['title:string(100)', 'author:string(80)'], + ); + + $db = $this->container->get(ConnectionInterface::class); + DbHelper::dropTable($db, 'book'); + + $command = $this->createCommand($this->container); + + try { + $exitCode = $command->setInputs(['yes'])->execute([]); + } catch (Throwable) { + } + + $output = $command->getDisplay(true); + + $this->assertFalse(isset($exitCode)); + $this->assertStringContainsString('>>> Total 0 out of 1 migration was reverted.', $output); + $this->assertStringContainsString('[ERROR] Not reverted.', $output); + } + + public function testRevertedButPartiallyApplied(): void + { + MigrationHelper::useMigrationsNamespace($this->container); + $createBookClass = MigrationHelper::createAndApplyMigration( + $this->container, + 'Create_Book', + 'table', + 'book', + ['title:string(100)', 'author:string(80)'], + ); + + $migrator = $this->container->get(Migrator::class); + $migrator->up(new M231017150317EmptyDown()); + + $command = $this->createCommand($this->container); + + try { + $exitCode = $command->setInputs(['yes'])->execute(['-a' => true]); + } catch (Throwable) { + } + + $output = $command->getDisplay(true); + + $this->assertFalse(isset($exitCode)); + $this->assertStringContainsString('>>> Total 1 out of 2 migrations were applied.', $output); + $this->assertStringContainsString('[ERROR] Reverted but partially applied.', $output); + + $this->container->get(Migrator::class)->down(new $createBookClass()); + $db = $this->container->get(ConnectionInterface::class); + DbHelper::dropTable($db, 'chapter'); + } + + public function testRevertedButNotApplied(): void + { + $migrator = $this->container->get(Migrator::class); + $migrator->up(new M231017150317EmptyDown()); + + $command = $this->createCommand($this->container); + + try { + $exitCode = $command->setInputs(['yes'])->execute([]); + } catch (Throwable) { + } + + $output = $command->getDisplay(true); + + $this->assertFalse(isset($exitCode)); + $this->assertStringContainsString('>>> Total 0 out of 1 migration was applied.', $output); + $this->assertStringContainsString('[ERROR] Reverted but not applied.', $output); + + $db = $this->container->get(ConnectionInterface::class); + DbHelper::dropTable($db, 'chapter'); + } + public function createCommand(ContainerInterface $container): CommandTester { return CommandHelper::getCommandTester($container, RedoCommand::class); diff --git a/tests/Common/Command/AbstractUpdateCommandTest.php b/tests/Common/Command/AbstractUpdateCommandTest.php index c9450ae3..79b961e1 100644 --- a/tests/Common/Command/AbstractUpdateCommandTest.php +++ b/tests/Common/Command/AbstractUpdateCommandTest.php @@ -9,8 +9,10 @@ use RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Tester\CommandTester; +use Throwable; use Yiisoft\Db\Connection\ConnectionInterface; use Yiisoft\Db\Migration\Command\UpdateCommand; +use Yiisoft\Db\Migration\Migrator; use Yiisoft\Db\Migration\Service\MigrationService; use Yiisoft\Db\Migration\Tests\Support\AssertTrait; use Yiisoft\Db\Migration\Tests\Support\Helper\CommandHelper; @@ -71,7 +73,7 @@ public function testExecuteWithPath(): void $this->assertStringContainsString('Apply the above migration y/n:', $output); $this->assertStringContainsString("Applying $className", $output); $this->assertStringContainsString(">>> [OK] - Applied $className", $output); - $this->assertStringContainsString('>>> 1 Migration was applied.', $output); + $this->assertStringContainsString('>>> Total 1 new migration was applied.', $output); } public function testExecuteWithNamespace(): void @@ -115,6 +117,12 @@ public function testExecuteWithNamespace(): void public function testExecuteExtended(): void { + $db = $this->container->get(ConnectionInterface::class); + + if ($db->getDriverName() === 'sqlite') { + self::markTestSkipped('Skipped due to issues #218 and #219.'); + } + MigrationHelper::useMigrationsPath($this->container); MigrationHelper::createMigration( @@ -143,17 +151,14 @@ public function testExecuteExtended(): void $exitCode = $command->execute([]); $output = $command->getDisplay(true); - $db = $this->container->get(ConnectionInterface::class); $dbSchema = $db->getSchema(); $departmentSchema = $dbSchema->getTableSchema('department'); $studentSchema = $dbSchema->getTableSchema('student'); - $this->assertSame(Command::SUCCESS, $exitCode); - /** Check create table department columns*/ $this->assertSame(Command::SUCCESS, $exitCode); $this->assertStringContainsString('Apply the above migrations y/n:', $output); - $this->assertStringContainsString('>>> 2 Migrations were applied.', $output); + $this->assertStringContainsString('>>> Total 2 new migrations were applied.', $output); /** Check table department field id */ $this->assertSame('id', $departmentSchema->getColumn('id')->getName()); @@ -229,7 +234,7 @@ public function testExecuteAgain(): void $output2 = $command2->getDisplay(true); $this->assertSame(Command::SUCCESS, $exitCode1); - $this->assertStringContainsString('1 Migration was applied.', $output1); + $this->assertStringContainsString('Total 1 new migration was applied.', $output1); $this->assertSame(Command::SUCCESS, $exitCode2); $this->assertStringContainsString('No new migrations found.', $output2); @@ -416,6 +421,74 @@ public function testIncorrectLimit(): void $this->assertStringContainsString('[ERROR] The limit option must be greater than 0.', $output); } + public function testPartiallyUpdated(): void + { + MigrationHelper::useMigrationsNamespace($this->container); + $createBookClass = MigrationHelper::createMigration( + $this->container, + '1Create_Book', + 'table', + 'book', + ['title:string(100)', 'author:string(80)'], + ); + MigrationHelper::createMigration( + $this->container, + '2Create_Book', + 'table', + 'book', + ['title:string(100)', 'author:string(80)'], + ); + + $command = $this->createCommand($this->container); + + try { + $exitCode = $command->setInputs(['yes'])->execute([]); + } catch (Throwable) { + } + + $output = $command->getDisplay(true); + + $this->assertFalse(isset($exitCode)); + $this->assertStringContainsString('>>> Total 1 out of 2 new migrations were applied.', $output); + $this->assertStringContainsString('[ERROR] Partially updated.', $output); + + $this->container->get(Migrator::class)->down(new $createBookClass()); + } + + public function testNotUpdated(): void + { + MigrationHelper::useMigrationsNamespace($this->container); + $createBookClass = MigrationHelper::createAndApplyMigration( + $this->container, + '1Create_Book', + 'table', + 'book', + ['title:string(100)', 'author:string(80)'], + ); + MigrationHelper::createMigration( + $this->container, + '2Create_Book', + 'table', + 'book', + ['title:string(100)', 'author:string(80)'], + ); + + $command = $this->createCommand($this->container); + + try { + $exitCode = $command->setInputs(['yes'])->execute([]); + } catch (Throwable) { + } + + $output = $command->getDisplay(true); + + $this->assertFalse(isset($exitCode)); + $this->assertStringContainsString('>>> Total 0 out of 1 new migration was applied.', $output); + $this->assertStringContainsString('[ERROR] Not updated.', $output); + + $this->container->get(Migrator::class)->down(new $createBookClass()); + } + public function createCommand(ContainerInterface $container): CommandTester { return CommandHelper::getCommandTester($container, UpdateCommand::class); diff --git a/tests/Support/Migrations/M231017150317EmptyDown.php b/tests/Support/Migrations/M231017150317EmptyDown.php new file mode 100644 index 00000000..e2ed652b --- /dev/null +++ b/tests/Support/Migrations/M231017150317EmptyDown.php @@ -0,0 +1,24 @@ +createTable('chapter', [ + 'id' => $b->primaryKey(), + 'name' => $b->string(100), + ]); + } + + public function down(MigrationBuilder $b): void + { + } +}