From e179723dafa800422948235d7cdfb037da836fd3 Mon Sep 17 00:00:00 2001 From: Mikulas Date: Fri, 17 Jul 2015 17:39:44 +0200 Subject: [PATCH] add full-rollback option for production migrations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Usecase: running migrations during deploy should not alter the database at all. It’s handy to only rollback the last migration during development, so the original behaviour is retained. Side effect: migrations are committed at once, not per single migration. Also affects original continue mode, not just full rollback mode. Original behaviour could have caused transient problems during deploy. --- .../SymfonyConsole/ContinueCommand.php | 5 +- src/Engine/Runner.php | 36 ++++++--- tests/cases/integration/Runner.Rollback.phpt | 76 +++++++++++++++++++ tests/fixtures/mysql/rollback/001.sql | 4 + tests/fixtures/mysql/rollback/002.sql | 1 + tests/fixtures/mysql/rollback/003.sql | 1 + tests/fixtures/pgsql/rollback/001.sql | 4 + tests/fixtures/pgsql/rollback/002.sql | 1 + tests/fixtures/pgsql/rollback/003.sql | 1 + 9 files changed, 118 insertions(+), 11 deletions(-) create mode 100644 tests/cases/integration/Runner.Rollback.phpt create mode 100644 tests/fixtures/mysql/rollback/001.sql create mode 100644 tests/fixtures/mysql/rollback/002.sql create mode 100644 tests/fixtures/mysql/rollback/003.sql create mode 100644 tests/fixtures/pgsql/rollback/001.sql create mode 100644 tests/fixtures/pgsql/rollback/002.sql create mode 100644 tests/fixtures/pgsql/rollback/003.sql diff --git a/src/Bridges/SymfonyConsole/ContinueCommand.php b/src/Bridges/SymfonyConsole/ContinueCommand.php index 7baf0df..5c69d48 100644 --- a/src/Bridges/SymfonyConsole/ContinueCommand.php +++ b/src/Bridges/SymfonyConsole/ContinueCommand.php @@ -25,13 +25,16 @@ protected function configure() $this->setDescription('Updates database schema by running all new migrations'); $this->setHelp("If table 'migrations' does not exist in current database, it is created automatically."); $this->addOption('production', NULL, InputOption::VALUE_NONE, 'Will not import dummy data'); + $this->addOption('full-rollback', 'r', InputOption::VALUE_NONE, 'Upon failing, rollback all migrations, not only the failed on. Only works reliably with PostgreSQL.'); } protected function execute(InputInterface $input, OutputInterface $output) { + $mode = $input->getOption('full-rollback') ? Runner::MODE_CONTINUE_FULL_ROLLBACK : Runner::MODE_CONTINUE; + $withDummy = !$input->getOption('production'); - $this->runMigrations(Runner::MODE_CONTINUE, $withDummy); + $this->runMigrations($mode, $withDummy); } } diff --git a/src/Engine/Runner.php b/src/Engine/Runner.php index 18527d5..86c7af9 100644 --- a/src/Engine/Runner.php +++ b/src/Engine/Runner.php @@ -25,6 +25,7 @@ class Runner { /** @const modes */ const MODE_CONTINUE = 'continue'; + const MODE_CONTINUE_FULL_ROLLBACK = 'continue-full-rollback'; const MODE_RESET = 'reset'; const MODE_INIT = 'init'; @@ -80,7 +81,7 @@ public function addExtensionHandler($extension, IExtensionHandler $handler) /** - * @param string $mode self::MODE_CONTINUE|self::MODE_RESET|self::MODE_INIT + * @param string $mode self::MODE_CONTINUE|self::MODE_CONTINUE_FULL_ROLLBACK|self::MODE_RESET|self::MODE_INIT * @return void */ public function run($mode = self::MODE_CONTINUE) @@ -103,15 +104,30 @@ public function run($mode = self::MODE_CONTINUE) $this->printer->printReset(); } - $this->driver->createTable(); - $migrations = $this->driver->getAllMigrations(); - $files = $this->finder->find($this->groups, array_keys($this->extensionsHandlers)); - $toExecute = $this->orderResolver->resolve($migrations, $this->groups, $files, $mode); - $this->printer->printToExecute($toExecute); - - foreach ($toExecute as $file) { - $queriesCount = $this->execute($file); - $this->printer->printExecute($file, $queriesCount); + $this->driver->beginTransaction(); + try { + $this->driver->createTable(); + $migrations = $this->driver->getAllMigrations(); + $files = $this->finder->find($this->groups, array_keys($this->extensionsHandlers)); + $toExecute = $this->orderResolver->resolve($migrations, $this->groups, $files, $mode); + $this->printer->printToExecute($toExecute); + + foreach ($toExecute as $file) { + $queriesCount = $this->execute($file); + $this->printer->printExecute($file, $queriesCount); + } + $this->driver->commitTransaction(); + + } catch (\Exception $e) { + if ($mode === self::MODE_CONTINUE_FULL_ROLLBACK) { + // rollback all migrations executed in this run + $this->driver->rollbackTransaction(); + + } else if ($mode === self::MODE_CONTINUE) { + // commit migrations not including the failing one + $this->driver->commitTransaction(); + } + throw $e; } $this->driver->unlock(); diff --git a/tests/cases/integration/Runner.Rollback.phpt b/tests/cases/integration/Runner.Rollback.phpt new file mode 100644 index 0000000..fdb4717 --- /dev/null +++ b/tests/cases/integration/Runner.Rollback.phpt @@ -0,0 +1,76 @@ +enabled = TRUE; + $rollback->name = 'rollback'; + $rollback->directory = $dir . '/rollback'; + $rollback->dependencies = []; + + return [$rollback]; + } + + public function testContinueRollbacksFailingOnly() + { + try { + $this->runner->run(Runner::MODE_CONTINUE); + } catch (\Exception $e) { + } + + $res = $this->dbal->query(' + SELECT Count(*) ' . $this->dbal->escapeIdentifier('count') . ' FROM information_schema.tables + WHERE TABLE_NAME = ' . $this->dbal->escapeString('rollback') . ' + AND table_catalog = ' . $this->dbal->escapeString('nextras_migrations_test') . ' + AND table_schema = ' . $this->dbal->escapeString($this->dbName) . ' + '); + $tableExists = (bool) $res[0]['count']; + + Assert::true($tableExists); + Assert::count(2, $this->driver->getAllMigrations()); + } + + public function testFullRollback() + { + $this->driver->createTable(); + + try { + $this->runner->run(Runner::MODE_CONTINUE_FULL_ROLLBACK); + } catch (\Exception $e) { + } + + $res = $this->dbal->query(' + SELECT Count(*) ' . $this->dbal->escapeIdentifier('count') . ' FROM information_schema.tables + WHERE TABLE_NAME = ' . $this->dbal->escapeString('rollback') . ' + AND table_catalog = ' . $this->dbal->escapeString('nextras_migrations_test') . ' + AND table_schema = ' . $this->dbal->escapeString($this->dbName) . ' + '); + $tableExists = (bool) $res[0]['count']; + + Assert::false($tableExists); + Assert::count(0, $this->driver->getAllMigrations()); + } + +} + + +(new RollbackTest)->run(); diff --git a/tests/fixtures/mysql/rollback/001.sql b/tests/fixtures/mysql/rollback/001.sql new file mode 100644 index 0000000..4f7f45b --- /dev/null +++ b/tests/fixtures/mysql/rollback/001.sql @@ -0,0 +1,4 @@ +CREATE TABLE `rollback` ( + `id` bigint NOT NULL, + PRIMARY KEY (`id`) +); diff --git a/tests/fixtures/mysql/rollback/002.sql b/tests/fixtures/mysql/rollback/002.sql new file mode 100644 index 0000000..097ad78 --- /dev/null +++ b/tests/fixtures/mysql/rollback/002.sql @@ -0,0 +1 @@ +INSERT INTO `rollback` (`id`) VALUES (1), (2), (3); diff --git a/tests/fixtures/mysql/rollback/003.sql b/tests/fixtures/mysql/rollback/003.sql new file mode 100644 index 0000000..6d0bc8b --- /dev/null +++ b/tests/fixtures/mysql/rollback/003.sql @@ -0,0 +1 @@ +INSERT INTO `rollback` (`id`) VALUES (3); -- duplicate key diff --git a/tests/fixtures/pgsql/rollback/001.sql b/tests/fixtures/pgsql/rollback/001.sql new file mode 100644 index 0000000..e4ed6e7 --- /dev/null +++ b/tests/fixtures/pgsql/rollback/001.sql @@ -0,0 +1,4 @@ +CREATE TABLE "rollback" ( + "id" serial4 NOT NULL, + PRIMARY KEY ("id") +); diff --git a/tests/fixtures/pgsql/rollback/002.sql b/tests/fixtures/pgsql/rollback/002.sql new file mode 100644 index 0000000..218ee8c --- /dev/null +++ b/tests/fixtures/pgsql/rollback/002.sql @@ -0,0 +1 @@ +INSERT INTO "rollback" ("id") VALUES (1), (2), (3); diff --git a/tests/fixtures/pgsql/rollback/003.sql b/tests/fixtures/pgsql/rollback/003.sql new file mode 100644 index 0000000..4741b95 --- /dev/null +++ b/tests/fixtures/pgsql/rollback/003.sql @@ -0,0 +1 @@ +INSERT INTO "rollback" ("id") VALUES (3); -- duplicate key