Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add full-rollback option for production migrations #19

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/Bridges/SymfonyConsole/ContinueCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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. <comment>Only works reliably with PostgreSQL.</comment>');
}


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);
}

}
36 changes: 26 additions & 10 deletions src/Engine/Runner.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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)
Expand All @@ -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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably wrong. It migrations contains more sqls and only the last one is wrong (throws exception), all previous queries are commited, aren't they?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've retained original transactions that wrap single file. This should only affect the new outer one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are actually right; this won't work with mysql (because it has terrible support for nested transactions). I will have to rewrite this for other than postgres driver which I initially had in mind when implementing this.

}
throw $e;
}

$this->driver->unlock();
Expand Down
73 changes: 73 additions & 0 deletions tests/cases/integration/Runner.Rollback.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

/**
* @testCase
* @dataProvider ../../dbals.ini
*/

namespace NextrasTests\Migrations;

use Mockery;
use Nextras\Migrations\Engine\Runner;
use Nextras\Migrations\Entities\Group;
use Tester;
use Tester\Assert;

require __DIR__ . '/../../bootstrap.php';


class RollbackTest extends IntegrationTestCase
{

protected function getGroups($dir)
{
$rollback = new Group();
$rollback->enabled = TRUE;
$rollback->name = 'rollback';
$rollback->directory = $dir . '/rollback';
$rollback->dependencies = [];

return [$rollback];
}


/**
* @param $mode
* @return bool table exists
*/
private function runInMode($mode)
{
try {
$this->runner->run($mode);
} 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_schema = ' . $this->dbal->escapeString($this->dbName) . '
');
return (bool) $res[0]['count'];
}


public function testContinueRollbacksFailingOnly()
{
Assert::true($this->runInMode(Runner::MODE_CONTINUE));
Assert::count(2, $this->driver->getAllMigrations());
}


public function testFullRollback()
{
$this->driver->createTable();

Assert::false($this->runInMode(Runner::MODE_CONTINUE_FULL_ROLLBACK));
Assert::count(0, $this->driver->getAllMigrations());
}

}


(new RollbackTest)->run();
4 changes: 4 additions & 0 deletions tests/fixtures/mysql/rollback/001.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CREATE TABLE `rollback` (
`id` bigint NOT NULL,
PRIMARY KEY (`id`)
);
1 change: 1 addition & 0 deletions tests/fixtures/mysql/rollback/002.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
INSERT INTO `rollback` (`id`) VALUES (1), (2), (3);
1 change: 1 addition & 0 deletions tests/fixtures/mysql/rollback/003.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
INSERT INTO `rollback` (`id`) VALUES (3); -- duplicate key
4 changes: 4 additions & 0 deletions tests/fixtures/pgsql/rollback/001.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CREATE TABLE "rollback" (
"id" serial4 NOT NULL,
PRIMARY KEY ("id")
);
1 change: 1 addition & 0 deletions tests/fixtures/pgsql/rollback/002.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
INSERT INTO "rollback" ("id") VALUES (1), (2), (3);
1 change: 1 addition & 0 deletions tests/fixtures/pgsql/rollback/003.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
INSERT INTO "rollback" ("id") VALUES (3); -- duplicate key