From 08882f5c5d0b6f11d69640cb896ac2cdcb55b6e9 Mon Sep 17 00:00:00 2001 From: Roman Havrylko Date: Tue, 27 Dec 2022 13:05:33 +0100 Subject: [PATCH 1/9] Implemented migration rollback. --- .../Command/Executor/RollbackExecutor.php | 190 +++++++++++++++ .../Command/MigrationDownCommand.php | 110 +-------- .../Command/MigrationMigrateCommand.php | 52 ++++- .../Command/MigrationStatusCommand.php | 23 ++ .../Generator/Manager/MigrationManager.php | 218 +++++++++++++++--- 5 files changed, 455 insertions(+), 138 deletions(-) create mode 100644 src/Propel/Generator/Command/Executor/RollbackExecutor.php diff --git a/src/Propel/Generator/Command/Executor/RollbackExecutor.php b/src/Propel/Generator/Command/Executor/RollbackExecutor.php new file mode 100644 index 0000000000..c8a0caad21 --- /dev/null +++ b/src/Propel/Generator/Command/Executor/RollbackExecutor.php @@ -0,0 +1,190 @@ +input = $input; + $this->output = $output; + $this->migrationManager = $migrationManager; + } + + /** + * @param list $previousTimestamps + * + * @return bool + */ + public function executeRollbackToPreviousVersion(array &$previousTimestamps): bool + { + $nextMigrationTimestamp = array_pop($previousTimestamps); + if (!$nextMigrationTimestamp) { + $this->output->writeln('No migration were ever executed on this database - nothing to reverse.'); + + return false; + } + + $this->output->writeln(sprintf( + 'Executing migration %s down', + $this->migrationManager->getMigrationClassName($nextMigrationTimestamp), + )); + + $nbPreviousTimestamps = count($previousTimestamps); + $previousTimestamp = 0; + if ($nbPreviousTimestamps) { + $previousTimestamp = $previousTimestamps[array_key_last($previousTimestamps)]; + } + + $migration = $this->migrationManager->getMigrationObject($nextMigrationTimestamp); + if (!$this->input->getOption(static::COMMAND_OPTION_FAKE) && $migration->preDown($this->migrationManager) === false) { + if (!$this->input->getOption(static::COMMAND_OPTION_FORCE)) { + $this->output->writeln('preDown() returned false. Aborting migration.'); + + return false; + } + + $this->output->writeln('preDown() returned false. Continue migration.'); + } + + foreach ($migration->getDownSQL() as $datasource => $sql) { + $this->executeRollbackForDatasource($datasource, $sql); + + $this->migrationManager->removeMigrationTimestamp($datasource, $nextMigrationTimestamp); + + if ($this->input->getOption(static::COMMAND_OPTION_VERBOSE)) { + $this->output->writeln(sprintf( + 'Downgraded migration date to %d for datasource "%s"', + $previousTimestamp, + $datasource, + )); + } + } + + if (!$this->input->getOption(static::COMMAND_OPTION_FAKE)) { + $migration->postDown($this->migrationManager); + } + + if ($nbPreviousTimestamps) { + $this->output->writeln(sprintf('Reverse migration complete. %d more migrations available for reverse.', $nbPreviousTimestamps)); + } else { + $this->output->writeln('Reverse migration complete. No more migration available for reverse'); + } + + return true; + } + + /** + * @param string $datasource + * @param string $sql + * + * @throws \Propel\Runtime\Exception\RuntimeException + * + * @return void + */ + protected function executeRollbackForDatasource(string $datasource, string $sql): void + { + $connection = $this->migrationManager->getConnection($datasource); + + if ($this->input->getOption(static::COMMAND_OPTION_VERBOSE)) { + $this->output->writeln(sprintf( + 'Connecting to database "%s" using DSN "%s"', + $datasource, + $connection['dsn'], + )); + } + + $conn = $this->migrationManager->getAdapterConnection($datasource); + $res = 0; + $statements = SqlParser::parseString($sql); + + if ($this->input->getOption(static::COMMAND_OPTION_FAKE)) { + return; + } + + foreach ($statements as $statement) { + try { + if ($this->input->getOption(static::COMMAND_OPTION_VERBOSE)) { + $this->output->writeln(sprintf('Executing statement "%s"', $statement)); + } + + $conn->exec($statement); + $res++; + } catch (Exception $e) { + if ($this->input->getOption(static::COMMAND_OPTION_FORCE)) { + //continue, but print error message + $this->output->writeln( + sprintf('Failed to execute SQL "%s". Continue migration.', $statement), + ); + } else { + throw new RuntimeException( + sprintf('Failed to execute SQL "%s". Aborting migration.', $statement), + 0, + $e, + ); + } + } + } + + $this->output->writeln(sprintf( + '%d of %d SQL statements executed successfully on datasource "%s"', + $res, + count($statements), + $datasource, + )); + } +} diff --git a/src/Propel/Generator/Command/MigrationDownCommand.php b/src/Propel/Generator/Command/MigrationDownCommand.php index af828af68f..ff16daf4a0 100644 --- a/src/Propel/Generator/Command/MigrationDownCommand.php +++ b/src/Propel/Generator/Command/MigrationDownCommand.php @@ -8,10 +8,8 @@ namespace Propel\Generator\Command; -use Exception; +use Propel\Generator\Command\Executor\RollbackExecutor; use Propel\Generator\Manager\MigrationManager; -use Propel\Generator\Util\SqlParser; -use Propel\Runtime\Exception\RuntimeException; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -41,8 +39,6 @@ protected function configure() /** * @inheritDoc - * - * @throws \Propel\Runtime\Exception\RuntimeException */ protected function execute(InputInterface $input, OutputInterface $output): int { @@ -79,108 +75,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int $manager->setWorkingDirectory($generatorConfig->getSection('paths')['migrationDir']); $previousTimestamps = $manager->getAlreadyExecutedMigrationTimestamps(); - $nextMigrationTimestamp = array_pop($previousTimestamps); - if (!$nextMigrationTimestamp) { - $output->writeln('No migration were ever executed on this database - nothing to reverse.'); - return static::CODE_ERROR; + $rollbackExecutor = new RollbackExecutor($input, $output, $manager); + if ($rollbackExecutor->executeRollbackToPreviousVersion($previousTimestamps)) { + return static::CODE_SUCCESS; } - $output->writeln(sprintf( - 'Executing migration %s down', - $manager->getMigrationClassName($nextMigrationTimestamp), - )); - - $nbPreviousTimestamps = count($previousTimestamps); - if ($nbPreviousTimestamps) { - $previousTimestamp = array_pop($previousTimestamps); - } else { - $previousTimestamp = 0; - } - - $migration = $manager->getMigrationObject($nextMigrationTimestamp); - - if (!$input->getOption('fake')) { - if ($migration->preDown($manager) === false) { - if ($input->getOption('force')) { - $output->writeln('preDown() returned false. Continue migration.'); - } else { - $output->writeln('preDown() returned false. Aborting migration.'); - - return static::CODE_ERROR; - } - } - } - - foreach ($migration->getDownSQL() as $datasource => $sql) { - $connection = $manager->getConnection($datasource); - - if ($input->getOption('verbose')) { - $output->writeln(sprintf( - 'Connecting to database "%s" using DSN "%s"', - $datasource, - $connection['dsn'], - )); - } - - $conn = $manager->getAdapterConnection($datasource); - $res = 0; - $statements = SqlParser::parseString($sql); - - if (!$input->getOption('fake')) { - foreach ($statements as $statement) { - try { - if ($input->getOption('verbose')) { - $output->writeln(sprintf('Executing statement "%s"', $statement)); - } - - $conn->exec($statement); - $res++; - } catch (Exception $e) { - if ($input->getOption('force')) { - //continue, but print error message - $output->writeln( - sprintf('Failed to execute SQL "%s". Continue migration.', $statement), - ); - } else { - throw new RuntimeException( - sprintf('Failed to execute SQL "%s". Aborting migration.', $statement), - 0, - $e, - ); - } - } - } - - $output->writeln(sprintf( - '%d of %d SQL statements executed successfully on datasource "%s"', - $res, - count($statements), - $datasource, - )); - } - - $manager->removeMigrationTimestamp($datasource, $nextMigrationTimestamp); - - if ($input->getOption('verbose')) { - $output->writeln(sprintf( - 'Downgraded migration date to %d for datasource "%s"', - $previousTimestamp, - $datasource, - )); - } - } - - if (!$input->getOption('fake')) { - $migration->postDown($manager); - } - - if ($nbPreviousTimestamps) { - $output->writeln(sprintf('Reverse migration complete. %d more migrations available for reverse.', $nbPreviousTimestamps)); - } else { - $output->writeln('Reverse migration complete. No more migration available for reverse'); - } - - return static::CODE_SUCCESS; + return static::CODE_ERROR; } } diff --git a/src/Propel/Generator/Command/MigrationMigrateCommand.php b/src/Propel/Generator/Command/MigrationMigrateCommand.php index a400338ada..cb9b79a4db 100644 --- a/src/Propel/Generator/Command/MigrationMigrateCommand.php +++ b/src/Propel/Generator/Command/MigrationMigrateCommand.php @@ -9,6 +9,7 @@ namespace Propel\Generator\Command; use Exception; +use Propel\Generator\Command\Executor\RollbackExecutor; use Propel\Generator\Manager\MigrationManager; use Propel\Generator\Util\SqlParser; use Propel\Runtime\Exception\RuntimeException; @@ -21,6 +22,16 @@ */ class MigrationMigrateCommand extends AbstractCommand { + /** + * @var string + */ + protected const COMMAND_OPTION_MIGRATE_TO_VERSION = 'migrate-to-version'; + + /** + * @var string + */ + protected const COMMAND_OPTION_MIGRATE_TO_VERSION_DESCRIPTION = 'Defines the version to migrate database.'; + /** * @inheritDoc */ @@ -34,6 +45,7 @@ protected function configure() ->addOption('connection', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Connection to use', []) ->addOption('fake', null, InputOption::VALUE_NONE, 'Does not touch the actual schema, but marks all migration as executed.') ->addOption('force', null, InputOption::VALUE_NONE, 'Continues with the migration even when errors occur.') + ->addOption(static::COMMAND_OPTION_MIGRATE_TO_VERSION, null, InputOption::VALUE_REQUIRED, static::COMMAND_OPTION_MIGRATE_TO_VERSION_DESCRIPTION) ->setName('migration:migrate') ->setAliases(['migrate']) ->setDescription('Execute all pending migrations'); @@ -78,13 +90,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int $manager->setMigrationTable($generatorConfig->getSection('migrations')['tableName']); $manager->setWorkingDirectory($generatorConfig->getSection('paths')['migrationDir']); + $version = $input->getOption(static::COMMAND_OPTION_MIGRATE_TO_VERSION); + if ($version && $manager->isDatabaseVersionApplied($version)) { + return $this->executeRollbackToVersion($input, $output, $manager, $version); + } + if (!$manager->getFirstUpMigrationTimestamp()) { $output->writeln('All migrations were already executed - nothing to migrate.'); return static::CODE_SUCCESS; } - $timestamps = $manager->getValidMigrationTimestamps(); + $timestamps = $manager->getValidMigrationTimestamps($version); if (count($timestamps) > 1) { $output->writeln(sprintf('%d migrations to execute', count($timestamps))); } @@ -188,4 +205,37 @@ protected function execute(InputInterface $input, OutputInterface $output): int return static::CODE_SUCCESS; } + + /** + * @param \Symfony\Component\Console\Input\InputInterface $input + * @param \Symfony\Component\Console\Output\OutputInterface $output + * @param \Propel\Generator\Manager\MigrationManager $migrationManager + * @param int $version + * + * @return int + */ + protected function executeRollbackToVersion( + InputInterface $input, + OutputInterface $output, + MigrationManager $migrationManager, + int $version + ): int { + $rollbackTimestamps = $migrationManager->getAlreadyExecutedMigrationTimestamps($version); + if ($rollbackTimestamps === []) { + $output->writeln(sprintf('The last executed version of the migration is %s - nothing to migrate.', $version)); + + return static::CODE_SUCCESS; + } + + $rollbackExecutor = new RollbackExecutor($input, $output, $migrationManager); + while ($rollbackTimestamps !== []) { + if (!$rollbackExecutor->executeRollbackToPreviousVersion($rollbackTimestamps)) { + return static::CODE_ERROR; + } + } + + $output->writeln(sprintf('The last executed version of the migration is %s.', $version)); + + return static::CODE_SUCCESS; + } } diff --git a/src/Propel/Generator/Command/MigrationStatusCommand.php b/src/Propel/Generator/Command/MigrationStatusCommand.php index 21f1b30e59..b1b61dc849 100644 --- a/src/Propel/Generator/Command/MigrationStatusCommand.php +++ b/src/Propel/Generator/Command/MigrationStatusCommand.php @@ -18,6 +18,16 @@ */ class MigrationStatusCommand extends AbstractCommand { + /** + * @var string + */ + protected const COMMAND_OPTION_LAST_VERSION = 'last-version'; + + /** + * @var string + */ + protected const COMMAND_OPTION_LAST_VERSION_DESCRIPTION = 'Use this option to receive the last executed version of the migration.'; + /** * @inheritDoc */ @@ -29,6 +39,7 @@ protected function configure() ->addOption('output-dir', null, InputOption::VALUE_REQUIRED, 'The output directory') ->addOption('migration-table', null, InputOption::VALUE_REQUIRED, 'Migration table name') ->addOption('connection', null, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Connection to use', []) + ->addOption(static::COMMAND_OPTION_LAST_VERSION, null, InputOption::VALUE_NONE, static::COMMAND_OPTION_LAST_VERSION_DESCRIPTION) ->setName('migration:status') ->setAliases(['status']) ->setDescription('Get migration status'); @@ -90,6 +101,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int )); } $manager->createMigrationTable($datasource); + } else { + $manager->modifyMigrationTableIfOutdated( + $manager->getAdapterConnection($datasource), + $manager->getPlatform($datasource), + ); } } @@ -106,6 +122,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } + if ($input->getOption(static::COMMAND_OPTION_LAST_VERSION) && $oldestMigrationTimestamp) { + $output->writeln(sprintf( + 'The last executed version of the migration is %s', + $oldestMigrationTimestamp, + )); + } + $output->writeln('Listing Migration files...'); $dir = $generatorConfig->getSection('paths')['migrationDir']; $migrationTimestamps = $manager->getMigrationTimestamps(); diff --git a/src/Propel/Generator/Manager/MigrationManager.php b/src/Propel/Generator/Manager/MigrationManager.php index 154e8892c0..f4fa099e2f 100644 --- a/src/Propel/Generator/Manager/MigrationManager.php +++ b/src/Propel/Generator/Manager/MigrationManager.php @@ -32,6 +32,16 @@ class MigrationManager extends AbstractManager { use PathTrait; + /** + * @var string + */ + protected const COL_VERSION = 'version'; + + /** + * @var string + */ + protected const COL_EXECUTION_DATETIME = 'execution_datetime'; + /** * @var array */ @@ -150,34 +160,27 @@ public function getAllDatabaseVersions(): array throw new Exception('You must define database connection settings in a buildtime-conf.xml file to use migrations'); } - /** @var array $migrationTimestamps */ $migrationTimestamps = []; foreach ($connections as $name => $params) { - $conn = $this->getAdapterConnection($name); - $platform = $this->getGeneratorConfig()->getConfiguredPlatform($conn); - if (!$platform->supportsMigrations()) { - continue; - } - - $sql = sprintf('SELECT version FROM %s', $this->getMigrationTable()); - try { - $stmt = $conn->prepare($sql); - $stmt->execute(); - - while ($migrationTimestamp = $stmt->fetchColumn()) { - /** @phpstan-var int $migrationTimestamp */ - $migrationTimestamps[] = $migrationTimestamp; - } + $migrationTimestamps += $this->getMigrationData($name); } catch (PDOException $e) { $this->createMigrationTable($name); $migrationTimestamps = []; } } - sort($migrationTimestamps); + usort($migrationTimestamps, function (array $a, array $b) { + if ($a[static::COL_EXECUTION_DATETIME] === $b[static::COL_EXECUTION_DATETIME]) { + return $a[static::COL_VERSION] <=> $b[static::COL_VERSION]; + } - return $migrationTimestamps; + return $a[static::COL_EXECUTION_DATETIME] <=> $b[static::COL_EXECUTION_DATETIME]; + }); + + return array_map(function (array $migrationTimestamp) { + return (int)$migrationTimestamp[static::COL_VERSION]; + }, $migrationTimestamps); } /** @@ -217,11 +220,9 @@ public function createMigrationTable(string $datasource): void $table = new Table($this->getMigrationTable()); $database->addTable($table); - $column = new Column('version'); - $column->getDomain()->copy($platform->getDomainForType('INTEGER')); - $column->setDefaultValue('0'); + $table->addColumn($this->createVersionColumn($platform)); + $table->addColumn($this->createExecutionDatetimeColumn($platform)); - $table->addColumn($column); // insert the table into the database $statements = $platform->getAddTableDDL($table); $conn = $this->getAdapterConnection($datasource); @@ -264,13 +265,21 @@ public function updateLatestMigrationTimestamp(string $datasource, int $timestam { $platform = $this->getPlatform($datasource); $conn = $this->getAdapterConnection($datasource); + + $this->modifyMigrationTableIfOutdated($conn, $platform); + $sql = sprintf( - 'INSERT INTO %s (%s) VALUES (?)', + 'INSERT INTO %s (%s, %s) VALUES (?, ?)', $this->getMigrationTable(), - $platform->doQuoting('version'), + $platform->doQuoting(static::COL_VERSION), + $platform->doQuoting(static::COL_EXECUTION_DATETIME), ); + + $executionDatetime = date('Y-m-d H:i:s'); + $stmt = $conn->prepare($sql); $stmt->bindParam(1, $timestamp, PDO::PARAM_INT); + $stmt->bindParam(2, $executionDatetime); $stmt->execute(); } @@ -295,14 +304,25 @@ public function getMigrationTimestamps(): array } /** - * @return array + * @param int|null $version + * + * @return list */ - public function getValidMigrationTimestamps(): array + public function getValidMigrationTimestamps(?int $version = null): array { $migrationTimestamps = array_diff($this->getMigrationTimestamps(), $this->getAllDatabaseVersions()); sort($migrationTimestamps); - return $migrationTimestamps; + if ($version === null) { + return $migrationTimestamps; + } + + $versionIndex = array_search($version, $migrationTimestamps, true); + if ($versionIndex === false) { + return $migrationTimestamps; + } + + return array_slice($migrationTimestamps, 0, $versionIndex + 1); } /** @@ -314,14 +334,30 @@ public function hasPendingMigrations(): bool } /** - * @return array + * @param int|null $version + * + * @return list */ - public function getAlreadyExecutedMigrationTimestamps(): array + public function getAlreadyExecutedMigrationTimestamps(?int $version = null): array { - $migrationTimestamps = array_intersect($this->getMigrationTimestamps(), $this->getAllDatabaseVersions()); - sort($migrationTimestamps); + $allDatabaseVersions = $this->getAllDatabaseVersions(); + $migrationTimestamps = array_intersect($this->getMigrationTimestamps(), $allDatabaseVersions); - return $migrationTimestamps; + $sortOrder = array_flip($allDatabaseVersions); + usort($migrationTimestamps, function (int $a, int $b) use ($sortOrder) { + return $sortOrder[$a] <=> $sortOrder[$b]; + }); + + if ($version === null) { + return $migrationTimestamps; + } + + $versionIndex = array_search($version, $migrationTimestamps, true); + if ($versionIndex === false) { + return $migrationTimestamps; + } + + return array_slice($migrationTimestamps, $versionIndex + 1); } /** @@ -504,4 +540,122 @@ public function getOldestDatabaseVersion(): ?int return array_pop($versions); } + + /** + * @param \Propel\Runtime\Connection\ConnectionInterface $connection + * @param \Propel\Generator\Platform\PlatformInterface $platform + * + * @return void + */ + public function modifyMigrationTableIfOutdated(ConnectionInterface $connection, PlatformInterface $platform): void + { + if ($this->columnExists($connection, static::COL_EXECUTION_DATETIME)) { + return; + } + + $table = new Table($this->getMigrationTable()); + + $column = $this->createExecutionDatetimeColumn($platform); + $column->setTable($table); + + /** @phpstan-var \Propel\Generator\Platform\DefaultPlatform $platform */ + $sql = $platform->getAddColumnDDL($column); + + $stmt = $connection->prepare($sql); + $stmt->execute(); + } + + /** + * @param int $version + * + * @return bool + */ + public function isDatabaseVersionApplied(int $version): bool + { + return in_array($version, $this->getAlreadyExecutedMigrationTimestamps()); + } + + /** + * @param string $connectionName + * + * @return array + */ + protected function getMigrationData(string $connectionName): array + { + $connection = $this->getAdapterConnection($connectionName); + $platform = $this->getGeneratorConfig()->getConfiguredPlatform($connection); + if (!$platform->supportsMigrations()) { + return []; + } + + $this->modifyMigrationTableIfOutdated($connection, $platform); + + $sql = sprintf( + 'SELECT %s, %s FROM %s', + static::COL_VERSION, + static::COL_EXECUTION_DATETIME, + $this->getMigrationTable(), + ); + + $stmt = $connection->prepare($sql); + $stmt->execute(); + + $migrationData = []; + while ($migrationRow = $stmt->fetch()) { + $migrationData[] = $migrationRow; + } + + return $migrationData; + } + + /** + * @param \Propel\Generator\Platform\PlatformInterface $platform + * + * @return \Propel\Generator\Model\Column + */ + protected function createVersionColumn(PlatformInterface $platform): Column + { + $column = new Column(static::COL_VERSION); + $column->getDomain()->copy($platform->getDomainForType('INTEGER')); + $column->setDefaultValue('0'); + + return $column; + } + + /** + * @param \Propel\Generator\Platform\PlatformInterface $platform + * + * @return \Propel\Generator\Model\Column + */ + protected function createExecutionDatetimeColumn(PlatformInterface $platform): Column + { + $column = new Column(static::COL_EXECUTION_DATETIME); + $column->getDomain()->copy($platform->getDomainForType('DATETIME')); + + return $column; + } + + /** + * @param \Propel\Runtime\Connection\ConnectionInterface $connection + * @param string $columnName + * + * @return bool + */ + protected function columnExists(ConnectionInterface $connection, string $columnName): bool + { + $sql = sprintf( + 'SELECT %s FROM %s', + $columnName, + $this->getMigrationTable(), + ); + + try { + $stmt = $connection->prepare($sql); + $stmt->execute(); + + return true; + } catch (PDOException $e) { + return false; + } + } } From 30da4e33b662c7a5f251a8d3efa7277d5fee26df Mon Sep 17 00:00:00 2001 From: Roman Havrylko Date: Tue, 27 Dec 2022 13:11:06 +0100 Subject: [PATCH 2/9] Created automated tests. --- .../Manager/MigrationManagerTest.php | 399 ++++++++++++++++-- 1 file changed, 363 insertions(+), 36 deletions(-) diff --git a/tests/Propel/Tests/Generator/Manager/MigrationManagerTest.php b/tests/Propel/Tests/Generator/Manager/MigrationManagerTest.php index 36c9c1c39c..a4b61d2461 100644 --- a/tests/Propel/Tests/Generator/Manager/MigrationManagerTest.php +++ b/tests/Propel/Tests/Generator/Manager/MigrationManagerTest.php @@ -8,8 +8,13 @@ namespace Propel\Tests\Generator\Manager; +use PDO; +use PDOException; use Propel\Generator\Config\GeneratorConfig; use Propel\Generator\Manager\MigrationManager; +use Propel\Generator\Model\Column; +use Propel\Generator\Model\Table; +use Propel\Generator\Platform\DefaultPlatform; use Propel\Tests\TestCase; /** @@ -18,9 +23,25 @@ class MigrationManagerTest extends TestCase { /** + * @uses \Propel\Generator\Manager\MigrationManager::COL_VERSION + * + * @var string + */ + private const COL_VERSION = 'version'; + + /** + * @uses \Propel\Generator\Manager\MigrationManager::COL_EXECUTION_DATETIME + * + * @var string + */ + private const COL_EXECUTION_DATETIME = 'execution_datetime'; + + /** + * @param list $migrationTimestamps + * * @return \Propel\Generator\Manager\MigrationManager */ - private function createMigrationManager(array $migrationTimestamps) + private function createMigrationManager(array $migrationTimestamps): MigrationManager { $generatorConfig = new GeneratorConfig(__DIR__ . '/../../../../Fixtures/migration/'); @@ -46,7 +67,7 @@ private function createMigrationManager(array $migrationTimestamps) /** * @return void */ - public function testMigrationTableWillBeCreated() + public function testMigrationTableWillBeCreated(): void { $migrationManager = $this->createMigrationManager([]); $this->assertFalse($migrationManager->migrationTableExists('migration')); @@ -56,30 +77,39 @@ public function testMigrationTableWillBeCreated() } /** + * @dataProvider getAllDatabaseVersionsDataProvider + * + * @param array $migrationData + * @param list $expectedDatabaseVersions + * * @return void */ - public function testGetAllDatabaseVersions() + public function testGetAllDatabaseVersions(array $migrationData, array $expectedDatabaseVersions): void { - $databaseVersions = [1, 2, 3]; $migrationManager = $this->createMigrationManager([]); $migrationManager->createMigrationTable('migration'); - foreach ($databaseVersions as $version) { - $migrationManager->updateLatestMigrationTimestamp('migration', $version); - } + $this->addMigrations($migrationManager, $migrationData); - $this->assertEquals($databaseVersions, $migrationManager->getAllDatabaseVersions()); + $this->assertSame($expectedDatabaseVersions, $migrationManager->getAllDatabaseVersions()); } /** + * @dataProvider getValidMigrationTimestampsDataProvider + * + * @param list $localTimestamps + * @param list $databaseTimestamps + * @param list $expectedTimestamps + * @param int|null $expectedVersion + * * @return void */ - public function testGetValidMigrationTimestamps() - { - $localTimestamps = [1, 2, 3, 4]; - $databaseTimestamps = [1, 2]; - $expectedMigrationTimestamps = [3, 4]; - + public function testGetValidMigrationTimestamps( + array $localTimestamps, + array $databaseTimestamps, + array $expectedTimestamps, + ?int $expectedVersion = null + ): void { $migrationManager = $this->createMigrationManager($localTimestamps); $migrationManager->createMigrationTable('migration'); @@ -87,13 +117,13 @@ public function testGetValidMigrationTimestamps() $migrationManager->updateLatestMigrationTimestamp('migration', $timestamp); } - $this->assertEquals($expectedMigrationTimestamps, $migrationManager->getValidMigrationTimestamps()); + $this->assertSame($expectedTimestamps, $migrationManager->getValidMigrationTimestamps($expectedVersion)); } /** * @return void */ - public function testRemoveMigrationTimestamp() + public function testRemoveMigrationTimestamp(): void { $localTimestamps = [1, 2]; $databaseTimestamps = [1, 2]; @@ -111,28 +141,33 @@ public function testRemoveMigrationTimestamp() } /** + * @dataProvider getAlreadyExecutedTimestampsDataProvider + * + * @param list $localTimestamps + * @param array $databaseMigrationData + * @param list $expectedTimestamps + * @param int|null $expectedVersion + * * @return void */ - public function testGetAlreadyExecutedTimestamps() - { - $timestamps = [1, 2]; - - $migrationManager = $this->createMigrationManager($timestamps); + public function testGetAlreadyExecutedTimestamps( + array $localTimestamps, + array $databaseMigrationData, + array $expectedTimestamps, + ?int $expectedVersion = null + ): void { + $migrationManager = $this->createMigrationManager($localTimestamps); $migrationManager->createMigrationTable('migration'); - $this->assertEquals([], $migrationManager->getAlreadyExecutedMigrationTimestamps()); + $this->addMigrations($migrationManager, $databaseMigrationData); - foreach ($timestamps as $timestamp) { - $migrationManager->updateLatestMigrationTimestamp('migration', $timestamp); - } - - $this->assertEquals($timestamps, $migrationManager->getAlreadyExecutedMigrationTimestamps()); + $this->assertSame($expectedTimestamps, $migrationManager->getAlreadyExecutedMigrationTimestamps($expectedVersion)); } /** * @return void */ - public function testIsPending() + public function testIsPending(): void { $localTimestamps = [1, 2]; @@ -149,7 +184,7 @@ public function testIsPending() /** * @return void */ - public function testGetOldestDatabaseVersion() + public function testGetOldestDatabaseVersion(): void { $timestamps = [1, 2]; $migrationManager = $this->createMigrationManager($timestamps); @@ -165,7 +200,7 @@ public function testGetOldestDatabaseVersion() /** * @return void */ - public function testGetFirstUpMigrationTimestamp() + public function testGetFirstUpMigrationTimestamp(): void { $migrationManager = $this->createMigrationManager([1, 2, 3]); $migrationManager->createMigrationTable('migration'); @@ -178,7 +213,7 @@ public function testGetFirstUpMigrationTimestamp() /** * @return void */ - public function testGetFirstDownMigrationTimestamp() + public function testGetFirstDownMigrationTimestamp(): void { $migrationManager = $this->createMigrationManager([1, 2, 3]); $migrationManager->createMigrationTable('migration'); @@ -192,7 +227,7 @@ public function testGetFirstDownMigrationTimestamp() /** * @return void */ - public function testGetCommentMigrationManager() + public function testGetCommentMigrationManager(): void { $migrationManager = $this->createMigrationManager([1, 2, 3]); @@ -204,18 +239,24 @@ public function testGetCommentMigrationManager() /** * @return void */ - public function testBuildVariableNamesFromConnectionNames() + public function testBuildVariableNamesFromConnectionNames(): void { - $manager = new class() extends MigrationManager{ + $manager = new class () extends MigrationManager { + /** + * @param array $migrationsUp + * @param array $migrationsDown + * + * @return array + */ public function build(array $migrationsUp, array $migrationsDown): array { return static::buildConnectionToVariableNameMap($migrationsUp, $migrationsDown); } }; - + $migrationsUp = array_fill_keys(['default', 'with space', '\/', '123'], ''); $migrationsDown = array_fill_keys(['default', 'connection$', 'connection&', 'connection%'], ''); - + $expectedResult = [ 'default' => '$connection_default', 'with space' => '$connection_withspace', @@ -228,4 +269,290 @@ public function build(array $migrationsUp, array $migrationsDown): array $result = $manager->build($migrationsUp, $migrationsDown); $this->assertEquals($expectedResult, $result); } + + /** + * @return void + */ + public function testCreateMigrationTableShouldTableWithColumns(): void + { + $migrationManager = $this->createMigrationManager([]); + $migrationManager->createMigrationTable('migration'); + + $this->assertTrue($migrationManager->migrationTableExists('migration')); + $this->assertTrue($this->columnExists($migrationManager, self::COL_VERSION)); + $this->assertTrue($this->columnExists($migrationManager, self::COL_EXECUTION_DATETIME)); + } + + /** + * @return void + */ + public function testUpdateLatestMigrationTimestamp(): void + { + $expectedVersion = 1; + $expectedExecutionDatetime = date('Y-m-d H:i:s'); + + $migrationManager = $this->createMigrationManager([]); + $migrationManager->createMigrationTable('migration'); + + $migrationManager->updateLatestMigrationTimestamp('migration', $expectedVersion); + + $connection = $migrationManager->getAdapterConnection('migration'); + $sql = sprintf( + 'SELECT %s, %s FROM %s WHERE %s=%s', + self::COL_VERSION, + self::COL_EXECUTION_DATETIME, + $migrationManager->getMigrationTable(), + self::COL_VERSION, + $expectedVersion, + ); + + $stmt = $connection->prepare($sql); + $stmt->execute(); + $migrationData = $stmt->fetch(); + + $this->assertSame($expectedVersion, (int)$migrationData[self::COL_VERSION]); + $this->assertGreaterThanOrEqual($expectedExecutionDatetime, $migrationData[self::COL_EXECUTION_DATETIME]); + } + + /** + * @return void + */ + public function testModifyMigrationTableIfOutdatedShouldNotUpdateTableIfExecutionDatetimeColumnExists(): void + { + $migrationManager = $this->createMigrationManager([]); + $migrationManager->createMigrationTable('migration'); + + $platformMock = $this->getMockBuilder(DefaultPlatform::class) + ->setMethods(['getAddColumnDDL']) + ->getMock(); + + $platformMock->expects($this->never())->method('getAddColumnDDL'); + + $migrationManager->modifyMigrationTableIfOutdated( + $migrationManager->getAdapterConnection('migration'), + $platformMock, + ); + } + + /** + * @return void + */ + public function testModifyMigrationTableShouldThrowExceptionIfMigrationTableDoesNotExist(): void + { + $migrationManager = $this->createMigrationManager([]); + + $this->expectException(PDOException::class); + + $migrationManager->modifyMigrationTableIfOutdated( + $migrationManager->getAdapterConnection('migration'), + $migrationManager->getPlatform('migration'), + ); + } + + /** + * @dataProvider isDatabaseVersionsAppliedDataProvider + * + * @param list $localTimestamps + * @param list $databaseTimestamps + * @param int $version + * @param bool $expectedIsDatabaseVersionApplied + * + * @return void + */ + public function testIsDatabaseVersionsApplied( + array $localTimestamps, + array $databaseTimestamps, + int $version, + bool $expectedIsDatabaseVersionApplied + ): void { + $migrationManager = $this->createMigrationManager($localTimestamps); + $migrationManager->createMigrationTable('migration'); + + foreach ($databaseTimestamps as $timestamp) { + $migrationManager->updateLatestMigrationTimestamp('migration', $timestamp); + } + + $this->assertSame($expectedIsDatabaseVersionApplied, $migrationManager->isDatabaseVersionApplied($version)); + } + + /** + * @return array>> + */ + public function getAllDatabaseVersionsDataProvider(): array + { + return [ + [ + [ + 1 => null, + 2 => null, + 3 => null, + ], + [1, 2, 3], + ], + [ + [ + 1 => date('Y-m-d H:i:s'), + 2 => date('Y-m-d H:i:s', strtotime('-1 day')), + 3 => date('Y-m-d H:i:s', strtotime('+1 day')), + ], + [2, 1, 3], + ], + [ + [ + 1 => date('Y-m-d H:i:s'), + 2 => date('Y-m-d H:i:s', strtotime('-1 day')), + 3 => date('Y-m-d H:i:s', strtotime('-1 day')), + ], + [2, 3, 1], + ], + [ + [ + 1 => null, + 2 => date('Y-m-d H:i:s', strtotime('+1 day')), + 3 => date('Y-m-d H:i:s'), + ], + [1, 3, 2], + ], + ]; + } + + /** + * @return array|int>> + */ + public function getValidMigrationTimestampsDataProvider(): array + { + return [ + 'The method should return full diff if a specific version is not provided.' => [ + [1, 2, 3], + [1, 2], + [3], + ], + 'The method should return full diff if the given version is not found in the intersection.' => [ + [1, 2, 3], + [1, 2], + [3], + 4, + ], + 'The method should cut all values from the diff after the given version.' => [ + [1, 2, 3, 4], + [1], + [2, 3], + 3, + ], + ]; + } + + /** + * @return array> + */ + public function getAlreadyExecutedTimestampsDataProvider(): array + { + return [ + 'The method should return an empty array if no intersection is found.' => [ + [1, 2, 3], + [], + [], + ], + 'The method should return full intersection if a specific version is not provided.' => [ + [1, 2, 3, 4], + [1 => null, 2 => null, 3 => null], + [1, 2, 3], + ], + 'The method should return the intersection according to the order of executed migrations.' => [ + [1, 2, 3, 4], + [1 => date('Y-m-d H:i:s'), 2 => null, 3 => null], + [2, 3, 1], + ], + 'The method should return a full intersection if the given version is not found in the intersection.' => [ + [1, 2, 3, 4], + [1 => null, 2 => null, 3 => null], + [1, 2, 3], + 4, + ], + 'The method should cut all values from the intersection before the given version.' => [ + [1, 2, 3, 4], + [1 => null, 2 => null, 3 => null], + [3], + 2, + ], + ]; + } + + /** + * @return array> + */ + public function isDatabaseVersionsAppliedDataProvider(): array + { + return [ + [ + [1, 2, 3], + [1, 2], + 4, + false, + ], + [ + [1, 2, 3], + [1, 2], + 1, + true, + ], + ]; + } + + /** + * @param \Propel\Generator\Manager\MigrationManager $migrationManager + * @param array $migrationData + * + * @return void + */ + private function addMigrations(MigrationManager $migrationManager, array $migrationData): void + { + $platform = $migrationManager->getPlatform('migration'); + $connection = $migrationManager->getAdapterConnection('migration'); + + foreach ($migrationData as $version => $executionDatetime) { + $sql = sprintf( + 'INSERT INTO %s (%s, %s) VALUES (?, ?)', + $migrationManager->getMigrationTable(), + $platform->doQuoting(self::COL_VERSION), + $platform->doQuoting(self::COL_EXECUTION_DATETIME), + ); + + $stmt = $connection->prepare($sql); + $stmt->bindParam(1, $version, PDO::PARAM_INT); + $stmt->bindParam( + 2, + $executionDatetime, + $executionDatetime === null ? PDO::PARAM_NULL : PDO::PARAM_STR, + ); + + $stmt->execute(); + } + } + + /** + * @param \Propel\Generator\Manager\MigrationManager $migrationManager + * @param string $columnName + * + * @return bool + */ + private function columnExists(MigrationManager $migrationManager, string $columnName): bool + { + $connection = $migrationManager->getAdapterConnection('migration'); + + $sql = sprintf( + 'SELECT %s FROM %s', + $columnName, + $migrationManager->getMigrationTable(), + ); + + try { + $stmt = $connection->prepare($sql); + $stmt->execute(); + + return true; + } catch (PDOException $e) { + return false; + } + } } From 2fb77192b3a72f5dbf174011d8719c657b905654 Mon Sep 17 00:00:00 2001 From: Roman Havrylko Date: Tue, 27 Dec 2022 15:47:57 +0100 Subject: [PATCH 3/9] Created automated tests. --- .../migrate-to-version/version-1/schema.xml | 20 ++ .../migrate-to-version/version-2/schema.xml | 21 ++ .../migrate-to-version/version-3/schema.xml | 22 ++ .../Tests/Generator/Command/MigrationTest.php | 259 +++++++++++++++++- 4 files changed, 312 insertions(+), 10 deletions(-) create mode 100644 tests/Fixtures/migrate-to-version/version-1/schema.xml create mode 100644 tests/Fixtures/migrate-to-version/version-2/schema.xml create mode 100644 tests/Fixtures/migrate-to-version/version-3/schema.xml diff --git a/tests/Fixtures/migrate-to-version/version-1/schema.xml b/tests/Fixtures/migrate-to-version/version-1/schema.xml new file mode 100644 index 0000000000..401969fc12 --- /dev/null +++ b/tests/Fixtures/migrate-to-version/version-1/schema.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + +
+ + + + + +
+ +
diff --git a/tests/Fixtures/migrate-to-version/version-2/schema.xml b/tests/Fixtures/migrate-to-version/version-2/schema.xml new file mode 100644 index 0000000000..ef8dcc1ed2 --- /dev/null +++ b/tests/Fixtures/migrate-to-version/version-2/schema.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + +
+ + + + + +
+ +
diff --git a/tests/Fixtures/migrate-to-version/version-3/schema.xml b/tests/Fixtures/migrate-to-version/version-3/schema.xml new file mode 100644 index 0000000000..a973fc0a91 --- /dev/null +++ b/tests/Fixtures/migrate-to-version/version-3/schema.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + +
+ + + + + +
+ +
diff --git a/tests/Propel/Tests/Generator/Command/MigrationTest.php b/tests/Propel/Tests/Generator/Command/MigrationTest.php index 9a90452d42..20698bca89 100644 --- a/tests/Propel/Tests/Generator/Command/MigrationTest.php +++ b/tests/Propel/Tests/Generator/Command/MigrationTest.php @@ -13,6 +13,7 @@ use Propel\Generator\Command\MigrationDiffCommand; use Propel\Generator\Command\MigrationDownCommand; use Propel\Generator\Command\MigrationMigrateCommand; +use Propel\Generator\Command\MigrationStatusCommand; use Propel\Generator\Command\MigrationUpCommand; use Propel\Runtime\Propel; use Propel\Tests\TestCaseFixturesDatabase; @@ -25,14 +26,56 @@ */ class MigrationTest extends TestCaseFixturesDatabase { + /** + * @var bool + */ private const MIGRATE_DOWN_AFTERWARDS = true; + + /** + * @var string + */ private const SCHEMA_DIR = __DIR__ . '/../../../../Fixtures/migration-command'; + + /** + * @var string + */ private const OUTPUT_DIR = __DIR__ . '/../../../../migrationdiff'; + /** + * @var string + */ + private const SCHEMA_DIR_MIGRATE_TO_VERSION = __DIR__ . '/../../../../Fixtures/migrate-to-version'; + + /** + * @see \Propel\Generator\Command\MigrationMigrateCommand::COMMAND_OPTION_MIGRATE_TO_VERSION + * + * @var string + */ + private const COMMAND_OPTION_MIGRATE_TO_VERSION = '--migrate-to-version'; + + /** + * @see \Propel\Generator\Command\MigrationStatusCommand::COMMAND_OPTION_LAST_VERSION + * + * @var string + */ + private const COMMAND_OPTION_LAST_VERSION = '--last-version'; + + /** + * @uses \Propel\Generator\Manager\MigrationManager::COL_VERSION + * + * @var string + */ + private const COL_VERSION = 'version'; + + /** + * @var string + */ + private const MIGRATION_TABLE = 'propel_migration'; + /** * @return void */ - public function testDiffCommandCreatesFiles() + public function testDiffCommandCreatesFiles(): void { $this->deleteMigrationFiles(); $this->runCommandAndAssertSuccess('migration:diff', new MigrationDiffCommand(), ['--schema-dir' => self::SCHEMA_DIR]); @@ -42,7 +85,7 @@ public function testDiffCommandCreatesFiles() /** * @return void */ - public function testDiffCommandCreatesSuffixedFiles() + public function testDiffCommandCreatesSuffixedFiles(): void { $this->deleteMigrationFiles(); $suffix = 'an_explanatory_filename_suffix'; @@ -53,7 +96,7 @@ public function testDiffCommandCreatesSuffixedFiles() /** * @return void */ - public function testCreateCommandCreatesFiles() + public function testCreateCommandCreatesFiles(): void { $this->deleteMigrationFiles(); $this->runCommandAndAssertSuccess('migration:create', new MigrationCreateCommand(), ['--schema-dir' => self::SCHEMA_DIR]); @@ -63,7 +106,7 @@ public function testCreateCommandCreatesFiles() /** * @return void */ - public function testCreateCommandCreatesSuffixedFiles() + public function testCreateCommandCreatesSuffixedFiles(): void { $this->deleteMigrationFiles(); $suffix = 'an_explanatory_filename_suffix'; @@ -74,7 +117,7 @@ public function testCreateCommandCreatesSuffixedFiles() /** * @return void */ - public function testUpCommandPerformsUpMigration() + public function testUpCommandPerformsUpMigration(): void { $outputString = $this->runCommandAndAssertSuccess('migration:up', new MigrationUpCommand(), [], self::MIGRATE_DOWN_AFTERWARDS); $this->assertStringContainsString('Migration complete.', $outputString); @@ -83,7 +126,7 @@ public function testUpCommandPerformsUpMigration() /** * @return void */ - public function testDownCommandPerformsDownMigration() + public function testDownCommandPerformsDownMigration(): void { $this->migrateUp(); $outputString = $this->runCommandAndAssertSuccess('migration:down', new MigrationDownCommand()); @@ -93,12 +136,135 @@ public function testDownCommandPerformsDownMigration() /** * @return void */ - public function testMigrateCommandPerformsUpMigration() + public function testMigrateCommandPerformsUpMigration(): void { $outputString = $this->runCommandAndAssertSuccess('migration:migrate', new MigrationMigrateCommand(), [], self::MIGRATE_DOWN_AFTERWARDS); $this->assertStringContainsString('Migration complete.', $outputString); } + /** + * @return void + */ + public function testMigrateCommandShouldMigrateToTheLastVersionIfTheGivenVersionIsNotExists(): void + { + $outputString = $this->runCommandAndAssertSuccess( + 'migration:migrate', + new MigrationMigrateCommand(), + [self::COMMAND_OPTION_MIGRATE_TO_VERSION => 0], + self::MIGRATE_DOWN_AFTERWARDS, + ); + + $this->assertStringContainsString('Migration complete.', $outputString); + } + + /** + * @return void + */ + public function testMigrateCommandShouldDoNothingIfGivenVersionIsTheLastAppliedVersion(): void + { + $this->setUpMigrateToVersion(); + + $migrationVersions = $this->getMigrationVersions(); + $expectedVersion = $migrationVersions[array_key_last($migrationVersions)]; + + $outputString = $this->runCommandAndAssertSuccess( + 'migration:migrate', + new MigrationMigrateCommand(), + [self::COMMAND_OPTION_MIGRATE_TO_VERSION => $expectedVersion], + ); + + $this->assertIsCurrentVersion($expectedVersion); + $this->assertStringContainsString( + sprintf('The last executed version of the migration is %s - nothing to migrate.', $expectedVersion), + $outputString, + ); + + $this->tearDownMigrateToVersion($migrationVersions); + } + + /** + * @return void + */ + public function testMigrateCommandShouldRollbackToTheGivenVersionIfItIsLowerThanTheCurrentVersion(): void + { + $this->setUpMigrateToVersion(); + + $migrationVersions = $this->getMigrationVersions(); + $expectedVersion = $migrationVersions[array_key_first($migrationVersions)]; + + $outputString = $this->runCommandAndAssertSuccess( + 'migration:migrate', + new MigrationMigrateCommand(), + [self::COMMAND_OPTION_MIGRATE_TO_VERSION => $expectedVersion], + ); + + $this->assertIsCurrentVersion($expectedVersion); + $this->assertStringContainsString( + sprintf('The last executed version of the migration is %s.', $expectedVersion), + $outputString, + ); + + $this->tearDownMigrateToVersion($migrationVersions); + } + + /** + * @return void + */ + public function testMigrateCommandShouldMigrateToTheGivenVersionIfItIsHigherThanTheCurrentVersion(): void + { + $this->setUpMigrateToVersion(); + + $migrationVersions = $this->getMigrationVersions(); + $this->migrateDown(); + + $expectedVersion = $migrationVersions[array_key_last($migrationVersions)]; + + $outputString = $this->runCommandAndAssertSuccess( + 'migration:migrate', + new MigrationMigrateCommand(), + [self::COMMAND_OPTION_MIGRATE_TO_VERSION => $expectedVersion], + ); + + $this->assertIsCurrentVersion($expectedVersion); + $this->assertStringContainsString('Migration complete. No further migration to execute.', $outputString); + + $this->tearDownMigrateToVersion($migrationVersions); + } + + /** + * @return void + */ + public function testMigrationStatusCommandShouldReturnTheLastMigrationVersionWhenOptionIsProvided(): void + { + $this->setUpMigrateToVersion(); + $this->migrateDown(); + + $outputString = $this->runCommandAndAssertSuccess( + 'migration:status', + new MigrationStatusCommand(), + [self::COMMAND_OPTION_LAST_VERSION => true], + ); + + $this->tearDownMigrateToVersion($this->getMigrationVersions()); + + $this->assertStringContainsString('The last executed version of the migration is', $outputString); + } + + /** + * @return void + */ + public function testMigrationStatusCommandShouldNotReturnTheLastMigrationVersionWhenOptionIsNotProvided(): void + { + $this->setUpMigrateToVersion(); + $this->migrateDown(); + + $outputString = $this->runCommandAndAssertSuccess('migration:status', new MigrationStatusCommand()); + + $this->tearDownMigrateToVersion($this->getMigrationVersions()); + + $this->assertStringNotContainsString('The last executed version of the migration is', $outputString); + } + /** * @return void */ @@ -118,7 +284,7 @@ private function deleteMigrationFiles(): void * @param array $additionalArguments * @param bool $migrateDownAfterwards * - * @return \Symfony\Component\Console\Output\StreamOutput + * @return string */ private function runCommandAndAssertSuccess( string $commandName, @@ -146,7 +312,7 @@ private function runCommandAndAssertSuccess( /** * @return void */ - private function migrateUp() + private function migrateUp(): void { $this->runCommand('migration:up', new MigrationUpCommand()); } @@ -154,7 +320,7 @@ private function migrateUp() /** * @return void */ - private function migrateDown() + private function migrateDown(): void { $this->runCommand('migration:down', new MigrationDownCommand()); } @@ -233,4 +399,77 @@ private function assertGeneratedFileContainsCreateTableStatement(bool $containsC $this->assertStringNotContainsString('CREATE TABLE ', $content); } } + + /** + * @param int $version + * + * @return void + */ + private function assertIsCurrentVersion(int $version): void + { + $sql = sprintf('SELECT %s FROM %s', self::COL_VERSION, self::MIGRATION_TABLE); + + $stmt = Propel::getServiceContainer()->getConnection()->prepare($sql); + $stmt->execute(); + + $versions = $stmt->fetchAll(); + $lastVersion = array_pop($versions)[self::COL_VERSION]; + + $this->assertSame($version, $lastVersion); + } + + /** + * @return void + */ + private function setUpMigrateToVersion(): void + { + $this->deleteMigrationFiles(); + + /** @var array $versionDirectories */ + $versionDirectories = glob( + sprintf( + '%s%s*', + self::SCHEMA_DIR_MIGRATE_TO_VERSION, + DIRECTORY_SEPARATOR, + ), + GLOB_ONLYDIR, + ); + + foreach ($versionDirectories as $versionDirectory) { + $this->runCommand('migration:diff', new MigrationDiffCommand(), ['--schema-dir' => $versionDirectory]); + $this->migrateUp(); + sleep(1); + } + } + + /** + * @param list $migrationVersions + * + * @return void + */ + private function tearDownMigrateToVersion(array $migrationVersions): void + { + foreach ($migrationVersions as $migrationVersion) { + $this->migrateDown(); + } + + $this->deleteMigrationFiles(); + } + + /** + * @return list + */ + private function getMigrationVersions(): array + { + $migrationFiles = scandir(sprintf('%s%s', self::OUTPUT_DIR, DIRECTORY_SEPARATOR)); + + $migrationVersions = []; + foreach ($migrationFiles as $migrationFile) { + if (preg_match('/^PropelMigration_(\d+).*\.php$/', $migrationFile, $matches)) { + $migrationVersions[] = (int)$matches[1]; + } + } + + return $migrationVersions; + } } From 418b1552574a6749ca05ebf6016f4368fe890d0b Mon Sep 17 00:00:00 2001 From: Roman Havrylko Date: Tue, 27 Dec 2022 15:54:47 +0100 Subject: [PATCH 4/9] Updated automated tests. --- tests/Propel/Tests/Generator/Command/MigrationTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Propel/Tests/Generator/Command/MigrationTest.php b/tests/Propel/Tests/Generator/Command/MigrationTest.php index 20698bca89..aca2475ada 100644 --- a/tests/Propel/Tests/Generator/Command/MigrationTest.php +++ b/tests/Propel/Tests/Generator/Command/MigrationTest.php @@ -415,7 +415,7 @@ private function assertIsCurrentVersion(int $version): void $versions = $stmt->fetchAll(); $lastVersion = array_pop($versions)[self::COL_VERSION]; - $this->assertSame($version, $lastVersion); + $this->assertSame($version, (int)$lastVersion); } /** From 92791c72b042490989ecd32f1cd66f5e03ffe228 Mon Sep 17 00:00:00 2001 From: Roman Havrylko Date: Thu, 29 Dec 2022 11:56:34 +0100 Subject: [PATCH 5/9] Fixes after CR. --- .../Command/Executor/RollbackExecutor.php | 6 ++--- .../Generator/Manager/MigrationManager.php | 7 ++++- .../Manager/MigrationManagerTest.php | 27 ++++++++++++------- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/Propel/Generator/Command/Executor/RollbackExecutor.php b/src/Propel/Generator/Command/Executor/RollbackExecutor.php index c8a0caad21..7d291d3434 100644 --- a/src/Propel/Generator/Command/Executor/RollbackExecutor.php +++ b/src/Propel/Generator/Command/Executor/RollbackExecutor.php @@ -159,7 +159,7 @@ protected function executeRollbackForDatasource(string $datasource, string $sql) foreach ($statements as $statement) { try { if ($this->input->getOption(static::COMMAND_OPTION_VERBOSE)) { - $this->output->writeln(sprintf('Executing statement "%s"', $statement)); + $this->output->writeln(sprintf('Executing statement `%s`', $statement)); } $conn->exec($statement); @@ -168,11 +168,11 @@ protected function executeRollbackForDatasource(string $datasource, string $sql) if ($this->input->getOption(static::COMMAND_OPTION_FORCE)) { //continue, but print error message $this->output->writeln( - sprintf('Failed to execute SQL "%s". Continue migration.', $statement), + sprintf('Failed to execute SQL `%s`. Continue migration.', $statement), ); } else { throw new RuntimeException( - sprintf('Failed to execute SQL "%s". Aborting migration.', $statement), + sprintf('Failed to execute SQL `%s`. Aborting migration.', $statement), 0, $e, ); diff --git a/src/Propel/Generator/Manager/MigrationManager.php b/src/Propel/Generator/Manager/MigrationManager.php index f4fa099e2f..6da54a9c7e 100644 --- a/src/Propel/Generator/Manager/MigrationManager.php +++ b/src/Propel/Generator/Manager/MigrationManager.php @@ -42,6 +42,11 @@ class MigrationManager extends AbstractManager */ protected const COL_EXECUTION_DATETIME = 'execution_datetime'; + /** + * @var string + */ + protected const EXECUTION_DATETIME_FORMAT = 'Y-m-d H:i:s'; + /** * @var array */ @@ -275,7 +280,7 @@ public function updateLatestMigrationTimestamp(string $datasource, int $timestam $platform->doQuoting(static::COL_EXECUTION_DATETIME), ); - $executionDatetime = date('Y-m-d H:i:s'); + $executionDatetime = date(static::EXECUTION_DATETIME_FORMAT); $stmt = $conn->prepare($sql); $stmt->bindParam(1, $timestamp, PDO::PARAM_INT); diff --git a/tests/Propel/Tests/Generator/Manager/MigrationManagerTest.php b/tests/Propel/Tests/Generator/Manager/MigrationManagerTest.php index a4b61d2461..28a3402cf7 100644 --- a/tests/Propel/Tests/Generator/Manager/MigrationManagerTest.php +++ b/tests/Propel/Tests/Generator/Manager/MigrationManagerTest.php @@ -36,6 +36,13 @@ class MigrationManagerTest extends TestCase */ private const COL_EXECUTION_DATETIME = 'execution_datetime'; + /** + * @uses \Propel\Generator\Manager\MigrationManager::EXECUTION_DATETIME_FORMAT + * + * @var string + */ + private const EXECUTION_DATETIME_FORMAT = 'Y-m-d H:i:s'; + /** * @param list $migrationTimestamps * @@ -289,7 +296,7 @@ public function testCreateMigrationTableShouldTableWithColumns(): void public function testUpdateLatestMigrationTimestamp(): void { $expectedVersion = 1; - $expectedExecutionDatetime = date('Y-m-d H:i:s'); + $expectedExecutionDatetime = date(self::EXECUTION_DATETIME_FORMAT); $migrationManager = $this->createMigrationManager([]); $migrationManager->createMigrationTable('migration'); @@ -391,25 +398,25 @@ public function getAllDatabaseVersionsDataProvider(): array ], [ [ - 1 => date('Y-m-d H:i:s'), - 2 => date('Y-m-d H:i:s', strtotime('-1 day')), - 3 => date('Y-m-d H:i:s', strtotime('+1 day')), + 1 => date(self::EXECUTION_DATETIME_FORMAT), + 2 => date(self::EXECUTION_DATETIME_FORMAT, strtotime('-1 day')), + 3 => date(self::EXECUTION_DATETIME_FORMAT, strtotime('+1 day')), ], [2, 1, 3], ], [ [ - 1 => date('Y-m-d H:i:s'), - 2 => date('Y-m-d H:i:s', strtotime('-1 day')), - 3 => date('Y-m-d H:i:s', strtotime('-1 day')), + 1 => date(self::EXECUTION_DATETIME_FORMAT), + 2 => date(self::EXECUTION_DATETIME_FORMAT, strtotime('-1 day')), + 3 => date(self::EXECUTION_DATETIME_FORMAT, strtotime('-1 day')), ], [2, 3, 1], ], [ [ 1 => null, - 2 => date('Y-m-d H:i:s', strtotime('+1 day')), - 3 => date('Y-m-d H:i:s'), + 2 => date(self::EXECUTION_DATETIME_FORMAT, strtotime('+1 day')), + 3 => date(self::EXECUTION_DATETIME_FORMAT), ], [1, 3, 2], ], @@ -460,7 +467,7 @@ public function getAlreadyExecutedTimestampsDataProvider(): array ], 'The method should return the intersection according to the order of executed migrations.' => [ [1, 2, 3, 4], - [1 => date('Y-m-d H:i:s'), 2 => null, 3 => null], + [1 => date(self::EXECUTION_DATETIME_FORMAT), 2 => null, 3 => null], [2, 3, 1], ], 'The method should return a full intersection if the given version is not found in the intersection.' => [ From 82e2c8bed8a450ff9e1f52984569321ed22518b5 Mon Sep 17 00:00:00 2001 From: Roman Havrylko Date: Thu, 29 Dec 2022 19:21:09 +0100 Subject: [PATCH 6/9] Fixes after CR. --- .../Command/Executor/RollbackExecutor.php | 72 +++++++------- .../Command/MigrationDownCommand.php | 25 ++++- .../Command/MigrationMigrateCommand.php | 20 ++-- .../Command/MigrationStatusCommand.php | 22 ++--- .../Generator/Manager/MigrationManager.php | 64 +++++++++---- .../Tests/Generator/Command/MigrationTest.php | 25 ++++- .../Manager/MigrationManagerTest.php | 94 +++++++++++++++---- 7 files changed, 222 insertions(+), 100 deletions(-) diff --git a/src/Propel/Generator/Command/Executor/RollbackExecutor.php b/src/Propel/Generator/Command/Executor/RollbackExecutor.php index 7d291d3434..09cc474966 100644 --- a/src/Propel/Generator/Command/Executor/RollbackExecutor.php +++ b/src/Propel/Generator/Command/Executor/RollbackExecutor.php @@ -66,33 +66,21 @@ public function __construct( } /** - * @param list $previousTimestamps + * @param int $currentVersion + * @param int|null $previousVersion * * @return bool */ - public function executeRollbackToPreviousVersion(array &$previousTimestamps): bool + public function executeRollbackToPreviousVersion(int $currentVersion, ?int $previousVersion = null): bool { - $nextMigrationTimestamp = array_pop($previousTimestamps); - if (!$nextMigrationTimestamp) { - $this->output->writeln('No migration were ever executed on this database - nothing to reverse.'); - - return false; - } - $this->output->writeln(sprintf( 'Executing migration %s down', - $this->migrationManager->getMigrationClassName($nextMigrationTimestamp), + $this->migrationManager->getMigrationClassName($currentVersion), )); - $nbPreviousTimestamps = count($previousTimestamps); - $previousTimestamp = 0; - if ($nbPreviousTimestamps) { - $previousTimestamp = $previousTimestamps[array_key_last($previousTimestamps)]; - } - - $migration = $this->migrationManager->getMigrationObject($nextMigrationTimestamp); - if (!$this->input->getOption(static::COMMAND_OPTION_FAKE) && $migration->preDown($this->migrationManager) === false) { - if (!$this->input->getOption(static::COMMAND_OPTION_FORCE)) { + $migration = $this->migrationManager->getMigrationObject($currentVersion); + if (!$this->isFake() && $migration->preDown($this->migrationManager) === false) { + if (!$this->isForce()) { $this->output->writeln('preDown() returned false. Aborting migration.'); return false; @@ -104,27 +92,21 @@ public function executeRollbackToPreviousVersion(array &$previousTimestamps): bo foreach ($migration->getDownSQL() as $datasource => $sql) { $this->executeRollbackForDatasource($datasource, $sql); - $this->migrationManager->removeMigrationTimestamp($datasource, $nextMigrationTimestamp); + $this->migrationManager->removeMigrationTimestamp($datasource, $currentVersion); - if ($this->input->getOption(static::COMMAND_OPTION_VERBOSE)) { + if ($this->isVerbose()) { $this->output->writeln(sprintf( 'Downgraded migration date to %d for datasource "%s"', - $previousTimestamp, + $previousVersion, $datasource, )); } } - if (!$this->input->getOption(static::COMMAND_OPTION_FAKE)) { + if (!$this->isFake()) { $migration->postDown($this->migrationManager); } - if ($nbPreviousTimestamps) { - $this->output->writeln(sprintf('Reverse migration complete. %d more migrations available for reverse.', $nbPreviousTimestamps)); - } else { - $this->output->writeln('Reverse migration complete. No more migration available for reverse'); - } - return true; } @@ -140,7 +122,7 @@ protected function executeRollbackForDatasource(string $datasource, string $sql) { $connection = $this->migrationManager->getConnection($datasource); - if ($this->input->getOption(static::COMMAND_OPTION_VERBOSE)) { + if ($this->isVerbose()) { $this->output->writeln(sprintf( 'Connecting to database "%s" using DSN "%s"', $datasource, @@ -152,20 +134,20 @@ protected function executeRollbackForDatasource(string $datasource, string $sql) $res = 0; $statements = SqlParser::parseString($sql); - if ($this->input->getOption(static::COMMAND_OPTION_FAKE)) { + if ($this->isFake()) { return; } foreach ($statements as $statement) { try { - if ($this->input->getOption(static::COMMAND_OPTION_VERBOSE)) { + if ($this->isVerbose()) { $this->output->writeln(sprintf('Executing statement `%s`', $statement)); } $conn->exec($statement); $res++; } catch (Exception $e) { - if ($this->input->getOption(static::COMMAND_OPTION_FORCE)) { + if ($this->isForce()) { //continue, but print error message $this->output->writeln( sprintf('Failed to execute SQL `%s`. Continue migration.', $statement), @@ -187,4 +169,28 @@ protected function executeRollbackForDatasource(string $datasource, string $sql) $datasource, )); } + + /** + * @return bool + */ + protected function isFake(): bool + { + return (bool)$this->input->getOption(static::COMMAND_OPTION_FAKE); + } + + /** + * @return bool + */ + protected function isForce(): bool + { + return (bool)$this->input->getOption(static::COMMAND_OPTION_FORCE); + } + + /** + * @return bool + */ + protected function isVerbose(): bool + { + return (bool)$this->input->getOption(static::COMMAND_OPTION_VERBOSE); + } } diff --git a/src/Propel/Generator/Command/MigrationDownCommand.php b/src/Propel/Generator/Command/MigrationDownCommand.php index ff16daf4a0..466dd92389 100644 --- a/src/Propel/Generator/Command/MigrationDownCommand.php +++ b/src/Propel/Generator/Command/MigrationDownCommand.php @@ -74,13 +74,30 @@ protected function execute(InputInterface $input, OutputInterface $output): int $manager->setMigrationTable($generatorConfig->getSection('migrations')['tableName']); $manager->setWorkingDirectory($generatorConfig->getSection('paths')['migrationDir']); - $previousTimestamps = $manager->getAlreadyExecutedMigrationTimestamps(); + $alreadyExecutedMigrations = $manager->getAlreadyExecutedMigrationTimestamps(); + if ($alreadyExecutedMigrations === []) { + $output->writeln('No migrations were ever executed on this database - nothing to reverse.'); + + return static::CODE_ERROR; + } $rollbackExecutor = new RollbackExecutor($input, $output, $manager); - if ($rollbackExecutor->executeRollbackToPreviousVersion($previousTimestamps)) { - return static::CODE_SUCCESS; + + $currentMigrationVersion = array_pop($alreadyExecutedMigrations); + + $leftMigrationsCount = count($alreadyExecutedMigrations); + $previousMigrationVersion = array_pop($alreadyExecutedMigrations); + + if (!$rollbackExecutor->executeRollbackToPreviousVersion($currentMigrationVersion, $previousMigrationVersion)) { + return static::CODE_ERROR; + } + + if ($leftMigrationsCount) { + $output->writeln(sprintf('Reverse migration complete. %d more migrations available for reverse.', $leftMigrationsCount)); + } else { + $output->writeln('Reverse migration complete. No more migration available for reverse'); } - return static::CODE_ERROR; + return static::CODE_SUCCESS; } } diff --git a/src/Propel/Generator/Command/MigrationMigrateCommand.php b/src/Propel/Generator/Command/MigrationMigrateCommand.php index cb9b79a4db..46240c5ad0 100644 --- a/src/Propel/Generator/Command/MigrationMigrateCommand.php +++ b/src/Propel/Generator/Command/MigrationMigrateCommand.php @@ -30,7 +30,7 @@ class MigrationMigrateCommand extends AbstractCommand /** * @var string */ - protected const COMMAND_OPTION_MIGRATE_TO_VERSION_DESCRIPTION = 'Defines the version to migrate database.'; + protected const COMMAND_OPTION_MIGRATE_TO_VERSION_DESCRIPTION = 'Defines the version to migrate database to.'; /** * @inheritDoc @@ -101,7 +101,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int return static::CODE_SUCCESS; } - $timestamps = $manager->getValidMigrationTimestamps($version); + $timestamps = $manager->getNonExecutedMigrationTimestampsByVersion($version); if (count($timestamps) > 1) { $output->writeln(sprintf('%d migrations to execute', count($timestamps))); } @@ -220,21 +220,25 @@ protected function executeRollbackToVersion( MigrationManager $migrationManager, int $version ): int { - $rollbackTimestamps = $migrationManager->getAlreadyExecutedMigrationTimestamps($version); - if ($rollbackTimestamps === []) { - $output->writeln(sprintf('The last executed version of the migration is %s - nothing to migrate.', $version)); + $alreadyExecutedMigrations = $migrationManager->getAlreadyExecutedMigrationTimestampsByVersion($version); + if ($alreadyExecutedMigrations === []) { + $output->writeln(sprintf('Already at version %s.', $version)); return static::CODE_SUCCESS; } $rollbackExecutor = new RollbackExecutor($input, $output, $migrationManager); - while ($rollbackTimestamps !== []) { - if (!$rollbackExecutor->executeRollbackToPreviousVersion($rollbackTimestamps)) { + + while ($alreadyExecutedMigrations !== []) { + $currentVersion = array_pop($alreadyExecutedMigrations); + $previousVersion = count($alreadyExecutedMigrations) ? $alreadyExecutedMigrations[array_key_last($alreadyExecutedMigrations)] : null; + + if (!$rollbackExecutor->executeRollbackToPreviousVersion($currentVersion, $previousVersion)) { return static::CODE_ERROR; } } - $output->writeln(sprintf('The last executed version of the migration is %s.', $version)); + $output->writeln(sprintf('Successfully rollback to migration version %s.', $version)); return static::CODE_SUCCESS; } diff --git a/src/Propel/Generator/Command/MigrationStatusCommand.php b/src/Propel/Generator/Command/MigrationStatusCommand.php index b1b61dc849..5a44ba9e28 100644 --- a/src/Propel/Generator/Command/MigrationStatusCommand.php +++ b/src/Propel/Generator/Command/MigrationStatusCommand.php @@ -26,7 +26,7 @@ class MigrationStatusCommand extends AbstractCommand /** * @var string */ - protected const COMMAND_OPTION_LAST_VERSION_DESCRIPTION = 'Use this option to receive the last executed version of the migration.'; + protected const COMMAND_OPTION_LAST_VERSION_DESCRIPTION = 'Use this option to receive the version of the last executed migration.'; /** * @inheritDoc @@ -83,6 +83,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int $manager->setMigrationTable($generatorConfig->getSection('migrations')['tableName']); $manager->setWorkingDirectory($generatorConfig->getSection('paths')['migrationDir']); + $oldestMigrationTimestamp = $manager->getOldestDatabaseVersion(); + if ($input->getOption(static::COMMAND_OPTION_LAST_VERSION)) { + $output->writeln((string)$oldestMigrationTimestamp); + + return static::CODE_SUCCESS; + } + $output->writeln('Checking Database Versions...'); foreach ($manager->getConnections() as $datasource => $params) { if ($input->getOption('verbose')) { @@ -102,14 +109,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $manager->createMigrationTable($datasource); } else { - $manager->modifyMigrationTableIfOutdated( - $manager->getAdapterConnection($datasource), - $manager->getPlatform($datasource), - ); + $manager->modifyMigrationTableIfOutdated($datasource); } } - $oldestMigrationTimestamp = $manager->getOldestDatabaseVersion(); if ($input->getOption('verbose')) { if ($oldestMigrationTimestamp) { $output->writeln(sprintf( @@ -122,13 +125,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } - if ($input->getOption(static::COMMAND_OPTION_LAST_VERSION) && $oldestMigrationTimestamp) { - $output->writeln(sprintf( - 'The last executed version of the migration is %s', - $oldestMigrationTimestamp, - )); - } - $output->writeln('Listing Migration files...'); $dir = $generatorConfig->getSection('paths')['migrationDir']; $migrationTimestamps = $manager->getMigrationTimestamps(); diff --git a/src/Propel/Generator/Manager/MigrationManager.php b/src/Propel/Generator/Manager/MigrationManager.php index 6da54a9c7e..d6c05f4d28 100644 --- a/src/Propel/Generator/Manager/MigrationManager.php +++ b/src/Propel/Generator/Manager/MigrationManager.php @@ -165,17 +165,17 @@ public function getAllDatabaseVersions(): array throw new Exception('You must define database connection settings in a buildtime-conf.xml file to use migrations'); } - $migrationTimestamps = []; + $migrationData = []; foreach ($connections as $name => $params) { try { - $migrationTimestamps += $this->getMigrationData($name); + $migrationData += $this->getMigrationData($name); } catch (PDOException $e) { $this->createMigrationTable($name); - $migrationTimestamps = []; + $migrationData = []; } } - usort($migrationTimestamps, function (array $a, array $b) { + usort($migrationData, function (array $a, array $b) { if ($a[static::COL_EXECUTION_DATETIME] === $b[static::COL_EXECUTION_DATETIME]) { return $a[static::COL_VERSION] <=> $b[static::COL_VERSION]; } @@ -183,9 +183,9 @@ public function getAllDatabaseVersions(): array return $a[static::COL_EXECUTION_DATETIME] <=> $b[static::COL_EXECUTION_DATETIME]; }); - return array_map(function (array $migrationTimestamp) { - return (int)$migrationTimestamp[static::COL_VERSION]; - }, $migrationTimestamps); + return array_map(function (array $migration) { + return (int)$migration[static::COL_VERSION]; + }, $migrationData); } /** @@ -271,7 +271,7 @@ public function updateLatestMigrationTimestamp(string $datasource, int $timestam $platform = $this->getPlatform($datasource); $conn = $this->getAdapterConnection($datasource); - $this->modifyMigrationTableIfOutdated($conn, $platform); + $this->modifyMigrationTableIfOutdated($datasource); $sql = sprintf( 'INSERT INTO %s (%s, %s) VALUES (?, ?)', @@ -309,14 +309,27 @@ public function getMigrationTimestamps(): array } /** + * @return array + */ + public function getValidMigrationTimestamps(): array + { + $migrationTimestamps = array_diff($this->getMigrationTimestamps(), $this->getAllDatabaseVersions()); + sort($migrationTimestamps); + + return $migrationTimestamps; + } + + /** + * - Gets non executed migrations. + * - If `version` is provided, filters out values after the given version in the result. + * * @param int|null $version * * @return list */ - public function getValidMigrationTimestamps(?int $version = null): array + public function getNonExecutedMigrationTimestampsByVersion(?int $version = null): array { - $migrationTimestamps = array_diff($this->getMigrationTimestamps(), $this->getAllDatabaseVersions()); - sort($migrationTimestamps); + $migrationTimestamps = $this->getValidMigrationTimestamps(); if ($version === null) { return $migrationTimestamps; @@ -339,11 +352,9 @@ public function hasPendingMigrations(): bool } /** - * @param int|null $version - * * @return list */ - public function getAlreadyExecutedMigrationTimestamps(?int $version = null): array + public function getAlreadyExecutedMigrationTimestamps(): array { $allDatabaseVersions = $this->getAllDatabaseVersions(); $migrationTimestamps = array_intersect($this->getMigrationTimestamps(), $allDatabaseVersions); @@ -353,6 +364,21 @@ public function getAlreadyExecutedMigrationTimestamps(?int $version = null): arr return $sortOrder[$a] <=> $sortOrder[$b]; }); + return $migrationTimestamps; + } + + /** + * - Gets already executed migration timestamps. + * - If `version` is provided, filters out values before the given version in the result. + * + * @param int|null $version + * + * @return list + */ + public function getAlreadyExecutedMigrationTimestampsByVersion(?int $version = null): array + { + $migrationTimestamps = $this->getAlreadyExecutedMigrationTimestamps(); + if ($version === null) { return $migrationTimestamps; } @@ -547,19 +573,21 @@ public function getOldestDatabaseVersion(): ?int } /** - * @param \Propel\Runtime\Connection\ConnectionInterface $connection - * @param \Propel\Generator\Platform\PlatformInterface $platform + * @param string $datasource * * @return void */ - public function modifyMigrationTableIfOutdated(ConnectionInterface $connection, PlatformInterface $platform): void + public function modifyMigrationTableIfOutdated(string $datasource): void { + $connection = $this->getAdapterConnection($datasource); + if ($this->columnExists($connection, static::COL_EXECUTION_DATETIME)) { return; } $table = new Table($this->getMigrationTable()); + $platform = $this->getPlatform($datasource); $column = $this->createExecutionDatetimeColumn($platform); $column->setTable($table); @@ -593,7 +621,7 @@ protected function getMigrationData(string $connectionName): array return []; } - $this->modifyMigrationTableIfOutdated($connection, $platform); + $this->modifyMigrationTableIfOutdated($connectionName); $sql = sprintf( 'SELECT %s, %s FROM %s', diff --git a/tests/Propel/Tests/Generator/Command/MigrationTest.php b/tests/Propel/Tests/Generator/Command/MigrationTest.php index aca2475ada..5374b4a719 100644 --- a/tests/Propel/Tests/Generator/Command/MigrationTest.php +++ b/tests/Propel/Tests/Generator/Command/MigrationTest.php @@ -175,7 +175,7 @@ public function testMigrateCommandShouldDoNothingIfGivenVersionIsTheLastAppliedV $this->assertIsCurrentVersion($expectedVersion); $this->assertStringContainsString( - sprintf('The last executed version of the migration is %s - nothing to migrate.', $expectedVersion), + sprintf('Already at version %s.', $expectedVersion), $outputString, ); @@ -200,7 +200,7 @@ public function testMigrateCommandShouldRollbackToTheGivenVersionIfItIsLowerThan $this->assertIsCurrentVersion($expectedVersion); $this->assertStringContainsString( - sprintf('The last executed version of the migration is %s.', $expectedVersion), + sprintf('Successfully rollback to migration version %s.', $expectedVersion), $outputString, ); @@ -231,13 +231,28 @@ public function testMigrateCommandShouldMigrateToTheGivenVersionIfItIsHigherThan $this->tearDownMigrateToVersion($migrationVersions); } + /** + * @return void + */ + public function testMigrationStatusCommandShouldReturnEmptyWhenOptionIsProvidedAndMigrationsWereNotExecuted(): void + { + $outputString = $this->runCommandAndAssertSuccess( + 'migration:status', + new MigrationStatusCommand(), + [self::COMMAND_OPTION_LAST_VERSION => true], + ); + + $this->assertEmpty(trim(str_replace('\n', '', $outputString))); + } + /** * @return void */ public function testMigrationStatusCommandShouldReturnTheLastMigrationVersionWhenOptionIsProvided(): void { $this->setUpMigrateToVersion(); - $this->migrateDown(); + + $migrationVersions = $this->getMigrationVersions(); $outputString = $this->runCommandAndAssertSuccess( 'migration:status', @@ -247,7 +262,7 @@ public function testMigrationStatusCommandShouldReturnTheLastMigrationVersionWhe $this->tearDownMigrateToVersion($this->getMigrationVersions()); - $this->assertStringContainsString('The last executed version of the migration is', $outputString); + $this->assertSame((string)array_pop($migrationVersions), trim(str_replace('\n', '', $outputString))); } /** @@ -262,7 +277,7 @@ public function testMigrationStatusCommandShouldNotReturnTheLastMigrationVersion $this->tearDownMigrateToVersion($this->getMigrationVersions()); - $this->assertStringNotContainsString('The last executed version of the migration is', $outputString); + $this->assertStringContainsString('Checking Database Versions', $outputString); } /** diff --git a/tests/Propel/Tests/Generator/Manager/MigrationManagerTest.php b/tests/Propel/Tests/Generator/Manager/MigrationManagerTest.php index 28a3402cf7..64a751ae2d 100644 --- a/tests/Propel/Tests/Generator/Manager/MigrationManagerTest.php +++ b/tests/Propel/Tests/Generator/Manager/MigrationManagerTest.php @@ -12,8 +12,6 @@ use PDOException; use Propel\Generator\Config\GeneratorConfig; use Propel\Generator\Manager\MigrationManager; -use Propel\Generator\Model\Column; -use Propel\Generator\Model\Table; use Propel\Generator\Platform\DefaultPlatform; use Propel\Tests\TestCase; @@ -102,7 +100,26 @@ public function testGetAllDatabaseVersions(array $migrationData, array $expected } /** - * @dataProvider getValidMigrationTimestampsDataProvider + * @return void + */ + public function testGetValidMigrationTimestamps(): void + { + $localTimestamps = [1, 2, 3, 4]; + $databaseTimestamps = [1, 2]; + $expectedMigrationTimestamps = [3, 4]; + + $migrationManager = $this->createMigrationManager($localTimestamps); + $migrationManager->createMigrationTable('migration'); + + foreach ($databaseTimestamps as $timestamp) { + $migrationManager->updateLatestMigrationTimestamp('migration', $timestamp); + } + + $this->assertEquals($expectedMigrationTimestamps, $migrationManager->getValidMigrationTimestamps()); + } + + /** + * @dataProvider getGetNonExecutedMigrationTimestampsByVersionDataProvider * * @param list $localTimestamps * @param list $databaseTimestamps @@ -111,7 +128,7 @@ public function testGetAllDatabaseVersions(array $migrationData, array $expected * * @return void */ - public function testGetValidMigrationTimestamps( + public function testGetNonExecutedMigrationTimestampsByVersion( array $localTimestamps, array $databaseTimestamps, array $expectedTimestamps, @@ -124,7 +141,7 @@ public function testGetValidMigrationTimestamps( $migrationManager->updateLatestMigrationTimestamp('migration', $timestamp); } - $this->assertSame($expectedTimestamps, $migrationManager->getValidMigrationTimestamps($expectedVersion)); + $this->assertSame($expectedTimestamps, $migrationManager->getNonExecutedMigrationTimestampsByVersion($expectedVersion)); } /** @@ -153,11 +170,33 @@ public function testRemoveMigrationTimestamp(): void * @param list $localTimestamps * @param array $databaseMigrationData * @param list $expectedTimestamps - * @param int|null $expectedVersion * * @return void */ public function testGetAlreadyExecutedTimestamps( + array $localTimestamps, + array $databaseMigrationData, + array $expectedTimestamps + ): void { + $migrationManager = $this->createMigrationManager($localTimestamps); + $migrationManager->createMigrationTable('migration'); + + $this->addMigrations($migrationManager, $databaseMigrationData); + + $this->assertSame($expectedTimestamps, $migrationManager->getAlreadyExecutedMigrationTimestamps()); + } + + /** + * @dataProvider getAlreadyExecutedMigrationTimestampsByVersionDataProvider + * + * @param list $localTimestamps + * @param array $databaseMigrationData + * @param list $expectedTimestamps + * @param int|null $expectedVersion + * + * @return void + */ + public function testGetAlreadyExecutedMigrationTimestampsByVersion( array $localTimestamps, array $databaseMigrationData, array $expectedTimestamps, @@ -168,7 +207,7 @@ public function testGetAlreadyExecutedTimestamps( $this->addMigrations($migrationManager, $databaseMigrationData); - $this->assertSame($expectedTimestamps, $migrationManager->getAlreadyExecutedMigrationTimestamps($expectedVersion)); + $this->assertSame($expectedTimestamps, $migrationManager->getAlreadyExecutedMigrationTimestampsByVersion($expectedVersion)); } /** @@ -326,19 +365,25 @@ public function testUpdateLatestMigrationTimestamp(): void */ public function testModifyMigrationTableIfOutdatedShouldNotUpdateTableIfExecutionDatetimeColumnExists(): void { - $migrationManager = $this->createMigrationManager([]); - $migrationManager->createMigrationTable('migration'); - $platformMock = $this->getMockBuilder(DefaultPlatform::class) ->setMethods(['getAddColumnDDL']) ->getMock(); + $migrationManager = $this->getMockBuilder(MigrationManager::class) + ->setMethods(['getPlatform']) + ->getMock(); + + $migrationManager->expects($this->any()) + ->method('getPlatform') + ->willReturn($platformMock); + + $generatorConfig = new GeneratorConfig(__DIR__ . '/../../../../Fixtures/migration/'); + $migrationManager->setConnections($generatorConfig->getBuildConnections()); + $migrationManager->setMigrationTable('migration'); + $platformMock->expects($this->never())->method('getAddColumnDDL'); - $migrationManager->modifyMigrationTableIfOutdated( - $migrationManager->getAdapterConnection('migration'), - $platformMock, - ); + $migrationManager->modifyMigrationTableIfOutdated('migration'); } /** @@ -350,10 +395,7 @@ public function testModifyMigrationTableShouldThrowExceptionIfMigrationTableDoes $this->expectException(PDOException::class); - $migrationManager->modifyMigrationTableIfOutdated( - $migrationManager->getAdapterConnection('migration'), - $migrationManager->getPlatform('migration'), - ); + $migrationManager->modifyMigrationTableIfOutdated('migration'); } /** @@ -426,7 +468,7 @@ public function getAllDatabaseVersionsDataProvider(): array /** * @return array|int>> */ - public function getValidMigrationTimestampsDataProvider(): array + public function getGetNonExecutedMigrationTimestampsByVersionDataProvider(): array { return [ 'The method should return full diff if a specific version is not provided.' => [ @@ -460,6 +502,20 @@ public function getAlreadyExecutedTimestampsDataProvider(): array [], [], ], + 'The method should return the intersection according to the order of executed migrations.' => [ + [1, 2, 3, 4], + [1 => date(self::EXECUTION_DATETIME_FORMAT), 2 => null, 3 => null], + [2, 3, 1], + ], + ]; + } + + /** + * @return array> + */ + public function getAlreadyExecutedMigrationTimestampsByVersionDataProvider(): array + { + return [ 'The method should return full intersection if a specific version is not provided.' => [ [1, 2, 3, 4], [1 => null, 2 => null, 3 => null], From 07b763f4bc7fb3ddd395e7ae368a011bc3421097 Mon Sep 17 00:00:00 2001 From: Roman Havrylko Date: Thu, 29 Dec 2022 20:09:57 +0100 Subject: [PATCH 7/9] Fixes after CR. --- src/Propel/Generator/Manager/MigrationManager.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Propel/Generator/Manager/MigrationManager.php b/src/Propel/Generator/Manager/MigrationManager.php index d6c05f4d28..fa69923c43 100644 --- a/src/Propel/Generator/Manager/MigrationManager.php +++ b/src/Propel/Generator/Manager/MigrationManager.php @@ -633,12 +633,7 @@ protected function getMigrationData(string $connectionName): array $stmt = $connection->prepare($sql); $stmt->execute(); - $migrationData = []; - while ($migrationRow = $stmt->fetch()) { - $migrationData[] = $migrationRow; - } - - return $migrationData; + return $stmt->fetchAll(); } /** From af2f4cc176fb7cc581b9d9ac8a2577fb3de09f88 Mon Sep 17 00:00:00 2001 From: Roman Havrylko Date: Tue, 3 Jan 2023 13:58:22 +0100 Subject: [PATCH 8/9] Updates after CR. --- .../Generator/Command/Executor/RollbackExecutor.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Propel/Generator/Command/Executor/RollbackExecutor.php b/src/Propel/Generator/Command/Executor/RollbackExecutor.php index 09cc474966..aaca17241b 100644 --- a/src/Propel/Generator/Command/Executor/RollbackExecutor.php +++ b/src/Propel/Generator/Command/Executor/RollbackExecutor.php @@ -79,13 +79,15 @@ public function executeRollbackToPreviousVersion(int $currentVersion, ?int $prev )); $migration = $this->migrationManager->getMigrationObject($currentVersion); - if (!$this->isFake() && $migration->preDown($this->migrationManager) === false) { - if (!$this->isForce()) { - $this->output->writeln('preDown() returned false. Aborting migration.'); - return false; - } + $canBeRollback = $this->isFake() || $migration->preDown($this->migrationManager); + if (!$canBeRollback && !$this->isForce()) { + $this->output->writeln('preDown() returned false. Aborting migration.'); + + return false; + } + if (!$canBeRollback) { $this->output->writeln('preDown() returned false. Continue migration.'); } From 74d175b248deedff1c90718d291cddee94a4a382 Mon Sep 17 00:00:00 2001 From: Roman Havrylko Date: Tue, 3 Jan 2023 14:03:28 +0100 Subject: [PATCH 9/9] Fixed CS. --- src/Propel/Generator/Command/Executor/RollbackExecutor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Propel/Generator/Command/Executor/RollbackExecutor.php b/src/Propel/Generator/Command/Executor/RollbackExecutor.php index aaca17241b..db10c3f427 100644 --- a/src/Propel/Generator/Command/Executor/RollbackExecutor.php +++ b/src/Propel/Generator/Command/Executor/RollbackExecutor.php @@ -80,7 +80,7 @@ public function executeRollbackToPreviousVersion(int $currentVersion, ?int $prev $migration = $this->migrationManager->getMigrationObject($currentVersion); - $canBeRollback = $this->isFake() || $migration->preDown($this->migrationManager); + $canBeRollback = $this->isFake() || $migration->preDown($this->migrationManager) !== false; if (!$canBeRollback && !$this->isForce()) { $this->output->writeln('preDown() returned false. Aborting migration.');