Skip to content

Commit

Permalink
Implement all or nothing transaction strategy for migrations.
Browse files Browse the repository at this point in the history
  • Loading branch information
jwage committed May 17, 2018
1 parent ead074a commit a18b89a
Show file tree
Hide file tree
Showing 24 changed files with 317 additions and 146 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ abstract class AbstractFileConfiguration extends Configuration
'migrations_directory',
'migrations',
'custom_template',
'all_or_nothing',
];

/** @var string */
Expand Down Expand Up @@ -108,11 +109,15 @@ protected function setConfiguration(array $config) : void
$this->loadMigrations($config['migrations']);
}

if (! isset($config['custom_template'])) {
if (isset($config['custom_template'])) {
$this->setCustomTemplate($config['custom_template']);
}

if (! isset($config['all_or_nothing'])) {
return;
}

$this->setCustomTemplate($config['custom_template']);
$this->setAllOrNothing($config['all_or_nothing']);
}

protected function getDirectoryRelativeToFile(string $file, string $input) : string
Expand Down
13 changes: 13 additions & 0 deletions lib/Doctrine/Migrations/Configuration/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ class Configuration
/** @var bool */
private $isDryRun = false;

/** @var bool */
private $allOrNothing = false;

/** @var Connection */
private $connection;

Expand Down Expand Up @@ -289,6 +292,16 @@ public function isDryRun() : bool
return $this->isDryRun;
}

public function setAllOrNothing(bool $allOrNothing) : void
{
$this->allOrNothing = $allOrNothing;
}

public function isAllOrNothing() : bool
{
return $this->allOrNothing;
}

public function isMigrationTableCreated() : bool
{
return $this->getDependencyFactory()->getMigrationTableStatus()->isCreated();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
</xs:simpleType>
</xs:element>
<xs:element type="xs:string" name="migrations-directory" minOccurs="0" maxOccurs="1"/>
<xs:element type="xs:string" name="all-or-nothing" minOccurs="0" maxOccurs="1"/>
<xs:element name="migrations" minOccurs="0" maxOccurs="1">
<xs:complexType>
<xs:sequence minOccurs="0" maxOccurs="unbounded">
Expand Down
4 changes: 4 additions & 0 deletions lib/Doctrine/Migrations/Configuration/XmlConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ protected function doLoad(string $file) : void
$config['migrations_directory'] = $this->getDirectoryRelativeToFile($file, (string) $xml->{'migrations-directory'});
}

if (isset($xml->{'all-or-nothing'})) {
$config['all_or_nothing'] = (bool) $xml->{'all-or-nothing'};
}

if (isset($xml->migrations->migration)) {
$migrations = [];

Expand Down
50 changes: 38 additions & 12 deletions lib/Doctrine/Migrations/Migrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Doctrine\Migrations\Exception\MigrationException;
use Doctrine\Migrations\Exception\NoMigrationsToExecute;
use Doctrine\Migrations\Exception\UnknownMigrationVersion;
use Throwable;
use const COUNT_RECURSIVE;
use function count;
use function sprintf;
Expand Down Expand Up @@ -44,7 +45,10 @@ public function setNoMigrationException(bool $noMigrationException = false) : vo
/** @return string[][] */
public function getSql(?string $to = null) : array
{
return $this->migrate($to, true);
$migratorConfig = (new MigratorConfig())
->setDryRun(true);

return $this->migrate($to, $migratorConfig);
}

public function writeSqlFile(string $path, ?string $to = null) : bool
Expand Down Expand Up @@ -81,10 +85,12 @@ public function writeSqlFile(string $path, ?string $to = null) : bool
*/
public function migrate(
?string $to = null,
bool $dryRun = false,
bool $timeAllQueries = false,
?callable $confirm = null
?MigratorConfig $migratorConfig = null
) : array {
$migratorConfig = $migratorConfig ?? new MigratorConfig();
$dryRun = $migratorConfig->isDryRun();
$confirm = $migratorConfig->getConfirm();

if ($to === null) {
$to = $this->migrationRepository->getLatestVersion();
}
Expand Down Expand Up @@ -134,19 +140,39 @@ public function migrate(
return $this->noMigrations();
}

$this->configuration->dispatchMigrationEvent(Events::onMigrationsMigrating, $direction, $dryRun);
$connection = $this->configuration->getConnection();

$allOrNothing = $migratorConfig->isAllOrNothing();

if ($allOrNothing) {
$connection->beginTransaction();
}

$sql = [];
$time = 0;
try {
$this->configuration->dispatchMigrationEvent(Events::onMigrationsMigrating, $direction, $dryRun);

foreach ($migrationsToExecute as $version) {
$versionExecutionResult = $version->execute($direction, $dryRun, $timeAllQueries);
$sql = [];
$time = 0;

$sql[$version->getVersion()] = $versionExecutionResult->getSql();
$time += $versionExecutionResult->getTime();
foreach ($migrationsToExecute as $version) {
$versionExecutionResult = $version->execute($direction, $migratorConfig);

$sql[$version->getVersion()] = $versionExecutionResult->getSql();
$time += $versionExecutionResult->getTime();
}

$this->configuration->dispatchMigrationEvent(Events::onMigrationsMigrated, $direction, $dryRun);
} catch (Throwable $e) {
if ($allOrNothing) {
$connection->rollBack();
}

throw $e;
}

$this->configuration->dispatchMigrationEvent(Events::onMigrationsMigrated, $direction, $dryRun);
if ($allOrNothing) {
$connection->commit();
}

$this->outputWriter->write("\n <comment>------------------------</comment>\n");
$this->outputWriter->write(sprintf(' <info>++</info> finished in %ss', $time));
Expand Down
83 changes: 83 additions & 0 deletions lib/Doctrine/Migrations/MigratorConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace Doctrine\Migrations;

class MigratorConfig
{
/** @var bool */
private $dryRun = false;

/** @var bool */
private $timeAllQueries = false;

/** @var bool */
private $noMigrationException = false;

/** @var bool */
private $allOrNothing = false;

/** @var callable|null */
private $confirm;

public function isDryRun() : bool
{
return $this->dryRun;
}

public function setDryRun(bool $dryRun) : self
{
$this->dryRun = $dryRun;

return $this;
}

public function getTimeAllQueries() : bool
{
return $this->timeAllQueries;
}

public function setTimeAllQueries(bool $timeAllQueries) : self
{
$this->timeAllQueries = $timeAllQueries;

return $this;
}

public function getNoMigrationException() : bool
{
return $this->noMigrationException;
}

public function setNoMigrationException(bool $noMigrationException = false) : self
{
$this->noMigrationException = $noMigrationException;

return $this;
}

public function isAllOrNothing() : bool
{
return $this->allOrNothing;
}

public function setAllOrNothing(bool $allOrNothing) : self
{
$this->allOrNothing = $allOrNothing;

return $this;
}

public function getConfirm() : ?callable
{
return $this->confirm;
}

public function setConfirm(callable $confirm) : self
{
$this->confirm = $confirm;

return $this;
}
}
25 changes: 19 additions & 6 deletions lib/Doctrine/Migrations/Tools/Console/Command/ExecuteCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Doctrine\Migrations\Tools\Console\Command;

use Doctrine\Migrations\MigratorConfig;
use Doctrine\Migrations\VersionDirection;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -86,7 +87,7 @@ protected function configure() : void
public function execute(InputInterface $input, OutputInterface $output) : int
{
$version = $input->getArgument('version');
$timeAllqueries = $input->getOption('query-time');
$timeAllQueries = $input->getOption('query-time');
$dryRun = $input->getOption('dry-run');
$path = $input->getOption('write-sql');
$direction = $input->getOption('down') !== false
Expand All @@ -103,16 +104,28 @@ public function execute(InputInterface $input, OutputInterface $output) : int
return 0;
}

$question = 'WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n)';
$cancelled = false;

if (! $this->canExecute($question, $input, $output)) {
$migratorConfig = (new MigratorConfig())
->setDryRun($dryRun)
->setTimeAllQueries($timeAllQueries)
->setConfirm(function () use ($input, $output, &$cancelled) {
$question = 'WARNING! You are about to execute a database migration that could result in schema changes and data lost. Are you sure you wish to continue? (y/n)';

$canContinue = $this->canExecute($question, $input, $output);
$cancelled = ! $canContinue;

return $canContinue;
})
;

$version->execute($direction, $migratorConfig);

if ($cancelled) {
$output->writeln('<error>Migration cancelled!</error>');

return 1;
}

$version->execute($direction, $dryRun, $timeAllqueries);

return 0;
}
}
39 changes: 31 additions & 8 deletions lib/Doctrine/Migrations/Tools/Console/Command/MigrateCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Doctrine\Migrations\Tools\Console\Command;

use Doctrine\Migrations\Migrator;
use Doctrine\Migrations\MigratorConfig;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
Expand Down Expand Up @@ -55,6 +56,13 @@ protected function configure() : void
InputOption::VALUE_NONE,
'Don\'t throw an exception if no migration is available (CI).'
)
->addOption(
'all-or-nothing',
null,
InputOption::VALUE_OPTIONAL,
'Wrap the entire migration in a transaction.',
false
)
->setHelp(<<<EOT
The <info>%command.name%</info> command executes a migration to a specified version or the latest available version:
Expand Down Expand Up @@ -88,6 +96,8 @@ protected function configure() : void
You can also time all the different queries if you wanna know which one is taking so long:
<info>%command.full_name% --query-time</info>
Use the --all-or-nothing option to wrap the entire migration in a transaction.
EOT
);

Expand All @@ -103,8 +113,18 @@ public function execute(InputInterface $input, OutputInterface $output) : int
$version = (string) $input->getArgument('version');
$path = $input->getOption('write-sql');
$allowNoMigration = (bool) $input->getOption('allow-no-migration');
$timeAllqueries = (bool) $input->getOption('query-time');
$timeAllQueries = (bool) $input->getOption('query-time');
$dryRun = (bool) $input->getOption('dry-run');
$allOrNothing = $input->getOption('all-or-nothing');

if ($allOrNothing !== false) {
$allOrNothing = $allOrNothing !== null
? (bool) $allOrNothing
: true
;
} else {
$allOrNothing = $this->configuration->isAllOrNothing();
}

$this->configuration->setIsDryRun($dryRun);

Expand Down Expand Up @@ -133,19 +153,22 @@ public function execute(InputInterface $input, OutputInterface $output) : int

$migrator->setNoMigrationException($allowNoMigration);

$result = $migrator->migrate(
$version,
$dryRun,
$timeAllqueries,
function () use ($input, $output, &$cancelled) {
$migratorConfig = (new MigratorConfig())
->setDryRun($dryRun)
->setTimeAllQueries($timeAllQueries)
->setNoMigrationException($allowNoMigration)
->setAllOrNothing($allOrNothing)
->setConfirm(function () use ($input, $output, &$cancelled) {
$question = 'WARNING! You are about to execute a database migration that could result in schema changes and data loss. Are you sure you wish to continue? (y/n)';

$canContinue = $this->canExecute($question, $input, $output);
$cancelled = ! $canContinue;

return $canContinue;
}
);
})
;

$migrator->migrate($version, $migratorConfig);

if ($cancelled) {
$output->writeln('<error>Migration cancelled!</error>');
Expand Down
Loading

0 comments on commit a18b89a

Please sign in to comment.