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 4c35bf1
Show file tree
Hide file tree
Showing 27 changed files with 390 additions and 143 deletions.
1 change: 1 addition & 0 deletions docs/en/reference/custom_configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ code that would looks like the following:
$configuration->setMigrationsDirectory('/path/to/project/src/App/Migrations');
$configuration->setMigrationsNamespace('App/Migrations');
$configuration->registerMigrationsFromDirectory('/path/to/project/src/App/Migrations');
$configuration->setAllOrNothing(true);
// My command that extends Doctrine\DBAL\Migrations\Tools\Console\Command\AbstractCommand
$command->setMigrationConfiguration($configuration);
3 changes: 3 additions & 0 deletions docs/en/reference/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ file like the following:
<migrations-directory>/path/to/migrations/classes/DoctrineMigrations</migrations-directory>
<all-or-nothing>1</all-or-nothing>
</doctrine-migrations>
Of course you could do the same thing with a *configuration.yml* file:
Expand All @@ -82,6 +84,7 @@ Of course you could do the same thing with a *configuration.yml* file:
column_name: version
executed_at_column_name: executed_at
migrations_directory: /path/to/migrations/classes/DoctrineMigrations
all_or_nothing: true
With the above example, the migrations tool will search the ``migrations_directory``
recursively for files that begin with ``Version`` followed one to 255 characters
Expand Down
79 changes: 74 additions & 5 deletions docs/en/reference/introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ commands to our `Doctrine Command Line Interface <http://doctrine-orm.readthedoc
new \Doctrine\DBAL\Migrations\Tools\Console\Command\VersionCommand()
));
Additionally you have to make sure the 'db' and 'dialog' Helpers are added to your Symfony
Additionally you have to make sure the ``db`` and ``dialog`` Helpers are added to your Symfony
Console HelperSet.

.. code-block:: php
Expand Down Expand Up @@ -185,18 +185,87 @@ the migrations are executed in the correct order.
While you *can* use custom filenames, it's probably a good idea to let Doctrine
:doc:`generate migration files </reference/generating_migrations>` for you.


And if you want to specify each migration manually in YAML you can:

.. code-block:: yaml
table_name: doctrine_migration_versions
migrations_directory: /path/to/migrations/classes/DoctrineMigrations
migrations:
migration1:
version: 20100704000000
class: DoctrineMigrations\NewMigration
migration1:
version: 20100704000000
class: DoctrineMigrations\NewMigration
If you specify your own migration classes (like `DoctrineMigrations\NewMigration` in the previous
example) you will need an autoloader unless all those classes begin with the prefix Version*,
for example path/to/migrations/classes/VersionNewMigration.php.

All or Nothing Migrations
~~~~~~~~~~~~~~~~~~~~~~~~~

If you want your migrations to be all or nothing then you can configure your migrations behave that way.
This means when you executed multiple migrations in a row, the whole migration will be wrapped
in a single migration and if one of the migrations fails, the transaction will be rolled back.

.. note::

This only works if the database connection you are using supports transactional DDL statements.

.. configuration-block::

.. code-block:: php
$configuration = new Configuration($connection);
$configuration->setAllOrNothing(true);
.. code-block:: xml
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-migrations xmlns="http://doctrine-project.org/schemas/migrations/configuration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/migrations/configuration
http://doctrine-project.org/schemas/migrations/configuration.xsd">
<name>Doctrine Sandbox Migrations</name>
<migrations-namespace>DoctrineMigrations</migrations-namespace>
<table name="doctrine_migration_versions" />
<migrations-directory>/path/to/migrations/classes/DoctrineMigrations</migrations-directory>
<all-or-nothing>1</all-or-nothing>
</doctrine-migrations>
.. code-block:: yaml
name: Doctrine Sandbox Migrations
migrations_namespace: DoctrineMigrations
table_name: doctrine_migration_versions
migrations_directory: /path/to/migrations/classes/DoctrineMigrations
all_or_nothing: true
.. code-block:: json
{
"name" : "Doctrine Sandbox Migrations",
"migrations_namespace" : "DoctrineMigrations",
"table_name" : "doctrine_migration_versions",
"migrations_directory" : "/path/to/migrations/classes/DoctrineMigrations",
"all_or_nothing" : true
}
You can also optionally use the ``--all-or-nothing`` option from the command line to enable or disable
the feature for a specific migration run:

.. code-block:: bash
$ ./doctrine migrations:migrate --all-or-nothing
Or if you have it enabled in your configuration setup and you want to disable it for a migration:

.. code-block:: bash
$ ./doctrine migrations:migrate --all-or-nothing=0
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;
}
}
Loading

0 comments on commit 4c35bf1

Please sign in to comment.