Skip to content

Commit 973e5b8

Browse files
committed
add full-rollback option for production migrations
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.
1 parent c184cf1 commit 973e5b8

File tree

9 files changed

+112
-11
lines changed

9 files changed

+112
-11
lines changed

src/Bridges/SymfonyConsole/ContinueCommand.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,16 @@ protected function configure()
2525
$this->setDescription('Updates database schema by running all new migrations');
2626
$this->setHelp("If table 'migrations' does not exist in current database, it is created automatically.");
2727
$this->addOption('production', NULL, InputOption::VALUE_NONE, 'Will not import dummy data');
28+
$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>');
2829
}
2930

3031

3132
protected function execute(InputInterface $input, OutputInterface $output)
3233
{
34+
$mode = $input->getOption('full-rollback') ? Runner::MODE_CONTINUE_FULL_ROLLBACK : Runner::MODE_CONTINUE;
35+
3336
$withDummy = !$input->getOption('production');
34-
$this->runMigrations(Runner::MODE_CONTINUE, $withDummy);
37+
$this->runMigrations($mode, $withDummy);
3538
}
3639

3740
}

src/Engine/Runner.php

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class Runner
2525
{
2626
/** @const modes */
2727
const MODE_CONTINUE = 'continue';
28+
const MODE_CONTINUE_FULL_ROLLBACK = 'continue-full-rollback';
2829
const MODE_RESET = 'reset';
2930
const MODE_INIT = 'init';
3031

@@ -80,7 +81,7 @@ public function addExtensionHandler($extension, IExtensionHandler $handler)
8081

8182

8283
/**
83-
* @param string $mode self::MODE_CONTINUE|self::MODE_RESET|self::MODE_INIT
84+
* @param string $mode self::MODE_CONTINUE|self::MODE_CONTINUE_FULL_ROLLBACK|self::MODE_RESET|self::MODE_INIT
8485
* @return void
8586
*/
8687
public function run($mode = self::MODE_CONTINUE)
@@ -103,15 +104,30 @@ public function run($mode = self::MODE_CONTINUE)
103104
$this->printer->printReset();
104105
}
105106

106-
$this->driver->createTable();
107-
$migrations = $this->driver->getAllMigrations();
108-
$files = $this->finder->find($this->groups, array_keys($this->extensionsHandlers));
109-
$toExecute = $this->orderResolver->resolve($migrations, $this->groups, $files, $mode);
110-
$this->printer->printToExecute($toExecute);
111-
112-
foreach ($toExecute as $file) {
113-
$queriesCount = $this->execute($file);
114-
$this->printer->printExecute($file, $queriesCount);
107+
$this->driver->beginTransaction();
108+
try {
109+
$this->driver->createTable();
110+
$migrations = $this->driver->getAllMigrations();
111+
$files = $this->finder->find($this->groups, array_keys($this->extensionsHandlers));
112+
$toExecute = $this->orderResolver->resolve($migrations, $this->groups, $files, $mode);
113+
$this->printer->printToExecute($toExecute);
114+
115+
foreach ($toExecute as $file) {
116+
$queriesCount = $this->execute($file);
117+
$this->printer->printExecute($file, $queriesCount);
118+
}
119+
$this->driver->commitTransaction();
120+
121+
} catch (\Exception $e) {
122+
if ($mode === self::MODE_CONTINUE_FULL_ROLLBACK) {
123+
// rollback all migrations executed in this run
124+
$this->driver->rollbackTransaction();
125+
126+
} else if ($mode === self::MODE_CONTINUE) {
127+
// commit migrations not including the failing one
128+
$this->driver->commitTransaction();
129+
}
130+
throw $e;
115131
}
116132

117133
$this->driver->unlock();
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
/**
4+
* @testCase
5+
* @dataProvider ../../dbals.ini
6+
*/
7+
8+
namespace NextrasTests\Migrations;
9+
10+
use Mockery;
11+
use Nextras\Migrations\Engine\Runner;
12+
use Nextras\Migrations\Entities\Group;
13+
use Tester;
14+
use Tester\Assert;
15+
16+
require __DIR__ . '/../../bootstrap.php';
17+
18+
19+
class RollbackTest extends IntegrationTestCase
20+
{
21+
22+
protected function getGroups($dir)
23+
{
24+
$rollback = new Group();
25+
$rollback->enabled = TRUE;
26+
$rollback->name = 'rollback';
27+
$rollback->directory = $dir . '/rollback';
28+
$rollback->dependencies = [];
29+
30+
return [$rollback];
31+
}
32+
33+
34+
/**
35+
* @param $mode
36+
* @return bool table exists
37+
*/
38+
private function runInMode($mode)
39+
{
40+
try {
41+
$this->runner->run($mode);
42+
} catch (\Exception $e) {
43+
}
44+
45+
$res = $this->dbal->query('
46+
SELECT Count(*) ' . $this->dbal->escapeIdentifier('count') . ' FROM information_schema.tables
47+
WHERE table_name = ' . $this->dbal->escapeString('rollback') . '
48+
AND table_schema = ' . $this->dbal->escapeString($this->dbName) . '
49+
');
50+
return (bool) $res[0]['count'];
51+
}
52+
53+
public function testContinueRollbacksFailingOnly()
54+
{
55+
Assert::true($this->runInMode(Runner::MODE_CONTINUE));
56+
Assert::count(2, $this->driver->getAllMigrations());
57+
}
58+
59+
public function testFullRollback()
60+
{
61+
$this->driver->createTable();
62+
63+
Assert::true($this->runInMode(Runner::MODE_CONTINUE_FULL_ROLLBACK));
64+
Assert::count(0, $this->driver->getAllMigrations());
65+
}
66+
67+
}
68+
69+
70+
(new RollbackTest)->run();

tests/fixtures/mysql/rollback/001.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CREATE TABLE `rollback` (
2+
`id` bigint NOT NULL,
3+
PRIMARY KEY (`id`)
4+
);

tests/fixtures/mysql/rollback/002.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INSERT INTO `rollback` (`id`) VALUES (1), (2), (3);

tests/fixtures/mysql/rollback/003.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INSERT INTO `rollback` (`id`) VALUES (3); -- duplicate key

tests/fixtures/pgsql/rollback/001.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CREATE TABLE "rollback" (
2+
"id" serial4 NOT NULL,
3+
PRIMARY KEY ("id")
4+
);

tests/fixtures/pgsql/rollback/002.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INSERT INTO "rollback" ("id") VALUES (1), (2), (3);

tests/fixtures/pgsql/rollback/003.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
INSERT INTO "rollback" ("id") VALUES (3); -- duplicate key

0 commit comments

Comments
 (0)