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 a86f9ec commit 9e23538
Show file tree
Hide file tree
Showing 24 changed files with 312 additions and 138 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ abstract class AbstractFileConfiguration extends Configuration
'migrations_directory',
'migrations',
'custom_template',
'all_or_nothing',
];

/** @var string */
Expand Down Expand Up @@ -113,11 +114,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 @@ -24,6 +24,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 @@ -65,6 +65,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
48 changes: 39 additions & 9 deletions lib/Doctrine/Migrations/Migrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Doctrine\Migrations\Exception\NoMigrationsToExecute;
use Doctrine\Migrations\Exception\UnknownMigrationVersion;
use Doctrine\Migrations\Tools\BytesFormatter;
use Throwable;
use const COUNT_RECURSIVE;
use function count;
use function sprintf;
Expand Down Expand Up @@ -50,7 +51,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 @@ -87,10 +91,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 @@ -144,15 +150,39 @@ public function migrate(

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

$sql = [];
$connection = $this->configuration->getConnection();

$allOrNothing = $migratorConfig->isAllOrNothing();

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

try {
$this->configuration->dispatchMigrationEvent(Events::onMigrationsMigrating, $direction, $dryRun);

$sql = [];
$time = 0;

foreach ($migrationsToExecute as $version) {
$versionExecutionResult = $version->execute($direction, $migratorConfig);

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

$sql[$version->getVersion()] = $versionExecutionResult->getSql();
$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();
}

$stopwatchEvent->stop();

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 @@ -87,7 +88,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 @@ -104,16 +105,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 @@ -56,6 +57,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 @@ -89,6 +97,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 @@ -104,8 +114,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 @@ -134,19 +154,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
11 changes: 6 additions & 5 deletions lib/Doctrine/Migrations/Version.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,10 @@ public function writeSqlFile(
string $path,
string $direction = VersionDirection::UP
) : bool {
$versionExecutionResult = $this->execute($direction, true);
$migratorConfig = (new MigratorConfig())
->setDryRun(true);

$versionExecutionResult = $this->execute($direction, $migratorConfig);

if (count($versionExecutionResult->getParams()) !== 0) {
throw MigrationNotConvertibleToSql::new($this->class);
Expand All @@ -160,15 +163,13 @@ public function writeSqlFile(

public function execute(
string $direction,
bool $dryRun = false,
bool $timeAllQueries = false
?MigratorConfig $migratorConfig = null
) : VersionExecutionResult {
return $this->versionExecutor->execute(
$this,
$this->migration,
$direction,
$dryRun,
$timeAllQueries
$migratorConfig
);
}

Expand Down
Loading

0 comments on commit 9e23538

Please sign in to comment.