Skip to content

Commit ab7ebf3

Browse files
authored
Merge pull request #878 from goetas/auto-update-schema
Handle migrated versions metadata table auto-update
2 parents c28f138 + 875b5de commit ab7ebf3

19 files changed

+527
-57
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Migrations\Exception;
6+
7+
use RuntimeException;
8+
9+
final class MetadataStorageError extends RuntimeException implements MigrationException
10+
{
11+
public static function notUpToDate() : self
12+
{
13+
return new self('The metadata storage is not up to date, please run the sync-metadata-storage command to fix this issue.');
14+
}
15+
16+
public static function notInitialized() : self
17+
{
18+
return new self('The metadata storage is not initialized, please run the sync-metadata-storage command to fix this issue.');
19+
}
20+
}

lib/Doctrine/Migrations/Metadata/Storage/MetadataStorage.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
interface MetadataStorage
1111
{
12+
public function ensureInitialized() : void;
13+
1214
public function getExecutedMigrations() : ExecutedMigrationsSet;
1315

1416
public function complete(ExecutionResult $migration) : void;

lib/Doctrine/Migrations/Metadata/Storage/TableMetadataStorage.php

Lines changed: 129 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,15 @@
99
use Doctrine\DBAL\Connections\MasterSlaveConnection;
1010
use Doctrine\DBAL\Platforms\AbstractPlatform;
1111
use Doctrine\DBAL\Schema\AbstractSchemaManager;
12+
use Doctrine\DBAL\Schema\Comparator;
1213
use Doctrine\DBAL\Schema\Table;
14+
use Doctrine\DBAL\Schema\TableDiff;
1315
use Doctrine\DBAL\Types\Type;
16+
use Doctrine\Migrations\Exception\MetadataStorageError;
17+
use Doctrine\Migrations\Metadata\AvailableMigration;
1418
use Doctrine\Migrations\Metadata\ExecutedMigration;
1519
use Doctrine\Migrations\Metadata\ExecutedMigrationsSet;
20+
use Doctrine\Migrations\MigrationRepository;
1621
use Doctrine\Migrations\Version\Direction;
1722
use Doctrine\Migrations\Version\ExecutionResult;
1823
use Doctrine\Migrations\Version\Version;
@@ -22,6 +27,8 @@
2227
use function floatval;
2328
use function round;
2429
use function sprintf;
30+
use function strlen;
31+
use function strpos;
2532
use function strtolower;
2633

2734
final class TableMetadataStorage implements MetadataStorage
@@ -38,46 +45,28 @@ final class TableMetadataStorage implements MetadataStorage
3845
/** @var TableMetadataStorageConfiguration */
3946
private $configuration;
4047

41-
public function __construct(Connection $connection, ?MetadataStorageConfiguration $configuration = null)
42-
{
43-
$this->connection = $connection;
44-
$this->schemaManager = $connection->getSchemaManager();
45-
$this->platform = $connection->getDatabasePlatform();
48+
/** @var MigrationRepository|null */
49+
private $migrationRepository;
50+
51+
public function __construct(
52+
Connection $connection,
53+
?MetadataStorageConfiguration $configuration = null,
54+
?MigrationRepository $migrationRepository = null
55+
) {
56+
$this->migrationRepository = $migrationRepository;
57+
$this->connection = $connection;
58+
$this->schemaManager = $connection->getSchemaManager();
59+
$this->platform = $connection->getDatabasePlatform();
4660

4761
if ($configuration !== null && ! ($configuration instanceof TableMetadataStorageConfiguration)) {
4862
throw new InvalidArgumentException(sprintf('%s accepts only %s as configuration', self::class, TableMetadataStorageConfiguration::class));
4963
}
5064
$this->configuration = $configuration ?: new TableMetadataStorageConfiguration();
5165
}
5266

53-
private function isInitialized() : bool
54-
{
55-
if ($this->connection instanceof MasterSlaveConnection) {
56-
$this->connection->connect('master');
57-
}
58-
59-
return $this->schemaManager->tablesExist([$this->configuration->getTableName()]);
60-
}
61-
62-
private function initialize() : void
63-
{
64-
$schemaChangelog = new Table($this->configuration->getTableName());
65-
66-
$schemaChangelog->addColumn($this->configuration->getVersionColumnName(), 'string', ['notnull' => true, 'length' => $this->configuration->getVersionColumnLength()]);
67-
$schemaChangelog->addColumn($this->configuration->getExecutedAtColumnName(), 'datetime', ['notnull' => false]);
68-
$schemaChangelog->addColumn($this->configuration->getExecutionTimeColumnName(), 'integer', ['notnull' => false]);
69-
70-
$schemaChangelog->setPrimaryKey([$this->configuration->getVersionColumnName()]);
71-
72-
$this->schemaManager->createTable($schemaChangelog);
73-
}
74-
7567
public function getExecutedMigrations() : ExecutedMigrationsSet
7668
{
77-
if (! $this->isInitialized()) {
78-
$this->initialize();
79-
}
80-
69+
$this->checkInitialization();
8170
$rows = $this->connection->fetchAll(sprintf('SELECT * FROM %s', $this->configuration->getTableName()));
8271

8372
$migrations = [];
@@ -109,9 +98,7 @@ public function getExecutedMigrations() : ExecutedMigrationsSet
10998

11099
public function reset() : void
111100
{
112-
if (! $this->isInitialized()) {
113-
$this->initialize();
114-
}
101+
$this->checkInitialization();
115102

116103
$this->connection->executeUpdate(
117104
sprintf(
@@ -123,9 +110,7 @@ public function reset() : void
123110

124111
public function complete(ExecutionResult $result) : void
125112
{
126-
if (! $this->isInitialized()) {
127-
$this->initialize();
128-
}
113+
$this->checkInitialization();
129114

130115
if ($result->getDirection() === Direction::DOWN) {
131116
$this->connection->delete($this->configuration->getTableName(), [
@@ -143,4 +128,111 @@ public function complete(ExecutionResult $result) : void
143128
]);
144129
}
145130
}
131+
132+
public function ensureInitialized() : void
133+
{
134+
$expectedSchemaChangelog = $this->getExpectedTable();
135+
136+
if (! $this->isInitialized($expectedSchemaChangelog)) {
137+
$this->schemaManager->createTable($expectedSchemaChangelog);
138+
139+
return;
140+
}
141+
142+
$diff = $this->needsUpdate($expectedSchemaChangelog);
143+
if ($diff === null) {
144+
return;
145+
}
146+
147+
$this->schemaManager->alterTable($diff);
148+
$this->updateMigratedVersionsFromV1orV2toV3();
149+
}
150+
151+
private function needsUpdate(Table $expectedTable) : ?TableDiff
152+
{
153+
$comparator = new Comparator();
154+
$currentTable = $this->schemaManager->listTableDetails($this->configuration->getTableName());
155+
$diff = $comparator->diffTable($currentTable, $expectedTable);
156+
157+
return $diff instanceof TableDiff ? $diff : null;
158+
}
159+
160+
private function isInitialized(Table $expectedTable) : bool
161+
{
162+
if ($this->connection instanceof MasterSlaveConnection) {
163+
$this->connection->connect('master');
164+
}
165+
166+
return $this->schemaManager->tablesExist([$expectedTable->getName()]);
167+
}
168+
169+
private function checkInitialization() : void
170+
{
171+
$expectedTable = $this->getExpectedTable();
172+
173+
if (! $this->isInitialized($expectedTable)) {
174+
throw MetadataStorageError::notInitialized();
175+
}
176+
177+
if ($this->needsUpdate($expectedTable)!== null) {
178+
throw MetadataStorageError::notUpToDate();
179+
}
180+
}
181+
182+
private function getExpectedTable() : Table
183+
{
184+
$schemaChangelog = new Table($this->configuration->getTableName());
185+
186+
$schemaChangelog->addColumn(
187+
$this->configuration->getVersionColumnName(),
188+
'string',
189+
['notnull' => true, 'length' => $this->configuration->getVersionColumnLength()]
190+
);
191+
$schemaChangelog->addColumn($this->configuration->getExecutedAtColumnName(), 'datetime', ['notnull' => false]);
192+
$schemaChangelog->addColumn($this->configuration->getExecutionTimeColumnName(), 'integer', ['notnull' => false]);
193+
194+
$schemaChangelog->setPrimaryKey([$this->configuration->getVersionColumnName()]);
195+
196+
return $schemaChangelog;
197+
}
198+
199+
private function updateMigratedVersionsFromV1orV2toV3() : void
200+
{
201+
if ($this->migrationRepository === null) {
202+
return;
203+
}
204+
205+
$availableMigrations = $this->migrationRepository->getMigrations()->getItems();
206+
$executedMigrations = $this->getExecutedMigrations()->getItems();
207+
208+
foreach ($availableMigrations as $availableMigration) {
209+
foreach ($executedMigrations as $k => $executedMigration) {
210+
if ($this->isAlreadyV3Format($availableMigration, $executedMigration)) {
211+
continue;
212+
}
213+
214+
$this->connection->update(
215+
$this->configuration->getTableName(),
216+
[
217+
$this->configuration->getVersionColumnName() => (string) $availableMigration->getVersion(),
218+
],
219+
[
220+
$this->configuration->getVersionColumnName() => (string) $executedMigration->getVersion(),
221+
]
222+
);
223+
unset($executedMigrations[$k]);
224+
}
225+
}
226+
}
227+
228+
private function isAlreadyV3Format(AvailableMigration $availableMigration, ExecutedMigration $executedMigration) : bool
229+
{
230+
return strpos(
231+
(string) $availableMigration->getVersion(),
232+
(string) $executedMigration->getVersion()
233+
) !== (
234+
strlen((string) $availableMigration->getVersion()) -
235+
strlen((string) $executedMigration->getVersion())
236+
);
237+
}
146238
}

lib/Doctrine/Migrations/Tools/Console/Command/ExecuteCommand.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ public function execute(InputInterface $input, OutputInterface $output) : ?int
132132
return 1;
133133
}
134134

135+
$this->getDependencyFactory()->getMetadataStorage()->ensureInitialized();
135136
$migrator->migrate($plan, $migratorConfiguration);
136137

137138
return 0;

lib/Doctrine/Migrations/Tools/Console/Command/MigrateCommand.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ public function execute(InputInterface $input, OutputInterface $output) : ?int
180180
return 3;
181181
}
182182

183+
$this->getDependencyFactory()->getMetadataStorage()->ensureInitialized();
183184
$migrator->migrate($plan, $migratorConfiguration);
184185

185186
return 0;

lib/Doctrine/Migrations/Tools/Console/Command/RollupCommand.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,17 @@ protected function configure() : void
3535
);
3636
}
3737

38-
public function execute(
39-
InputInterface $input,
40-
OutputInterface $output
41-
) : ?int {
38+
public function execute(InputInterface $input, OutputInterface $output) : ?int
39+
{
40+
$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)';
41+
42+
if (! $this->canExecute($question, $input, $output)) {
43+
$output->writeln('<error>Migration cancelled!</error>');
44+
45+
return 3;
46+
}
47+
48+
$this->getDependencyFactory()->getMetadataStorage()->ensureInitialized();
4249
$version = $this->getDependencyFactory()->getRollup()->rollup();
4350

4451
$output->writeln(sprintf(
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Migrations\Tools\Console\Command;
6+
7+
use Symfony\Component\Console\Input\InputInterface;
8+
use Symfony\Component\Console\Output\OutputInterface;
9+
10+
class SyncMetadataCommand extends DoctrineCommand
11+
{
12+
/** @var string */
13+
protected static $defaultName = 'migrations:sync-metadata-storage';
14+
15+
protected function configure() : void
16+
{
17+
parent::configure();
18+
19+
$this
20+
->setAliases(['sync-metadata-storage'])
21+
->setDescription('Ensures that the metadata storage is at the latest version.')
22+
->setHelp(<<<EOT
23+
The <info>%command.name%</info> command updates metadata storage the latest version.
24+
25+
<info>%command.full_name%</info>
26+
EOT
27+
);
28+
}
29+
30+
public function execute(
31+
InputInterface $input,
32+
OutputInterface $output
33+
) : int {
34+
$this->getDependencyFactory()->getMetadataStorage()->ensureInitialized();
35+
36+
$output->writeln('Metadata storage synchronized');
37+
38+
return 0;
39+
}
40+
}

lib/Doctrine/Migrations/Tools/Console/Command/UpToDateCommand.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Doctrine\Migrations\Tools\Console\Command;
66

7+
use Doctrine\Migrations\Exception\MetadataStorageError;
78
use Symfony\Component\Console\Input\InputInterface;
89
use Symfony\Component\Console\Input\InputOption;
910
use Symfony\Component\Console\Output\OutputInterface;
@@ -39,7 +40,16 @@ public function execute(InputInterface $input, OutputInterface $output) : ?int
3940
{
4041
$statusCalculator = $this->getDependencyFactory()->getMigrationStatusCalculator();
4142

42-
$executedUnavailableMigrations = $statusCalculator->getExecutedUnavailableMigrations();
43+
try {
44+
$executedUnavailableMigrations = $statusCalculator->getExecutedUnavailableMigrations();
45+
} catch (MetadataStorageError $metadataStorageError) {
46+
$output->writeln(sprintf(
47+
'<error>%s</error>',
48+
$metadataStorageError->getMessage()
49+
));
50+
51+
return 3;
52+
}
4353

4454
$newMigrations = $statusCalculator->getNewMigrations();
4555

lib/Doctrine/Migrations/Tools/Console/ConsoleRunner.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Doctrine\Migrations\Tools\Console\Command\MigrateCommand;
1414
use Doctrine\Migrations\Tools\Console\Command\RollupCommand;
1515
use Doctrine\Migrations\Tools\Console\Command\StatusCommand;
16+
use Doctrine\Migrations\Tools\Console\Command\SyncMetadataCommand;
1617
use Doctrine\Migrations\Tools\Console\Command\UpToDateCommand;
1718
use Doctrine\Migrations\Tools\Console\Command\VersionCommand;
1819
use PackageVersions\Versions;
@@ -59,6 +60,7 @@ public static function addCommands(Application $cli) : void
5960
new StatusCommand(),
6061
new VersionCommand(),
6162
new UpToDateCommand(),
63+
new SyncMetadataCommand(),
6264
]);
6365

6466
if (! $cli->getHelperSet()->has('em')) {

0 commit comments

Comments
 (0)