diff --git a/phalcon-migrations b/phalcon-migrations index d326e6f..3ac7ea6 100644 --- a/phalcon-migrations +++ b/phalcon-migrations @@ -35,16 +35,16 @@ try { $command = new Migration($parser); $command->run(); } catch (CommandsException $commandsException) { - echo Color::error($commandsException->getMessage()); + echo Color::error($commandsException->getMessage(), 'Exception Error: '); exit(1); } catch (RuntimeException $runtimeException) { - echo Color::error($runtimeException->getMessage()); + echo Color::error($runtimeException->getMessage(), 'Runtime Error: '); exit(1); } catch (DbException $dbException) { - echo Color::error($dbException->getMessage()); + echo Color::error($dbException->getMessage(), 'DB Error: '); exit(1); } } catch (Throwable $e) { - fwrite(STDERR, 'FATAL ERROR: ' . $e->getMessage() . PHP_EOL); + echo Color::fatal($e->getMessage()); exit(1); } diff --git a/src/Console/Color.php b/src/Console/Color.php index f325932..dde1691 100644 --- a/src/Console/Color.php +++ b/src/Console/Color.php @@ -174,11 +174,12 @@ public static function head(string $msg): string * * @static * @param string $msg + * @param string $prefix * @return string */ - public static function error(string $msg): string + public static function error(string $msg, string $prefix = 'Error: '): string { - $msg = 'Error: ' . $msg; + $msg = $prefix . $msg; $space = self::tabSpaces($msg); $out = static::colorize(str_pad(' ', $space), Color::FG_WHITE, Color::AT_BOLD, Color::BG_RED) . PHP_EOL; $out .= static::colorize(' ' . $msg . ' ', Color::FG_WHITE, Color::AT_BOLD, Color::BG_RED) . PHP_EOL; @@ -187,6 +188,25 @@ public static function error(string $msg): string return $out; } + /** + * Color style for fatal error messages. + * + * @static + * @param string $msg + * @param string $prefix + * @return string + */ + public static function fatal(string $msg, string $prefix = 'Fatal Error: '): string + { + $msg = $prefix . $msg; + $space = self::tabSpaces($msg); + $out = static::colorize(str_pad(' ', $space), Color::FG_LIGHT_GRAY, Color::AT_BOLD, Color::BG_RED) . PHP_EOL; + $out .= static::colorize(' ' . $msg . ' ', Color::FG_LIGHT_GRAY, Color::AT_BOLD, Color::BG_RED) . PHP_EOL; + $out .= static::colorize(str_pad(' ', $space), Color::FG_LIGHT_GRAY, Color::AT_BOLD, Color::BG_RED) . PHP_EOL; + + return $out; + } + /** * Color style for success messages. * diff --git a/src/Console/Commands/Migration.php b/src/Console/Commands/Migration.php index 154f31f..d79e303 100644 --- a/src/Console/Commands/Migration.php +++ b/src/Console/Commands/Migration.php @@ -14,14 +14,12 @@ namespace Phalcon\Migrations\Console\Commands; use Phalcon\Config; -use Phalcon\Cop\Parser; -use Phalcon\Migrations\Console\Color; -use Phalcon\Migrations\Migrations; -use Phalcon\Migrations\Script\ScriptException; -use Phalcon\Mvc\Model\Exception; use Phalcon\Config\Adapter\Ini as IniConfig; use Phalcon\Config\Adapter\Json as JsonConfig; use Phalcon\Config\Adapter\Yaml as YamlConfig; +use Phalcon\Cop\Parser; +use Phalcon\Migrations\Console\Color; +use Phalcon\Migrations\Migrations; /** * Migration Command @@ -69,8 +67,8 @@ public function getPossibleParams(): array /** * @throws CommandsException - * @throws ScriptException - * @throws Exception + * @throws \Phalcon\Db\Exception + * @throws \Exception */ public function run(): void { @@ -146,15 +144,16 @@ public function run(): void break; case 'run': Migrations::run([ - 'directory' => $path, - 'tableName' => $tableName, - 'migrationsDir' => $migrationsDir, - 'force' => $this->parser->has('force'), - 'tsBased' => $migrationsTsBased, - 'config' => $config, - 'version' => $this->parser->get('version'), - 'migrationsInDb' => $migrationsInDb, - 'verbose' => $this->parser->has('verbose'), + 'directory' => $path, + 'tableName' => $tableName, + 'migrationsDir' => $migrationsDir, + 'force' => $this->parser->has('force'), + 'tsBased' => $migrationsTsBased, + 'config' => $config, + 'version' => $this->parser->get('version'), + 'migrationsInDb' => $migrationsInDb, + 'verbose' => $this->parser->has('verbose'), + 'skip-foreign-checks' => $this->parser->has('skip-foreign-checks'), ]); break; case 'list': diff --git a/src/Migration/Action/Generate.php b/src/Migration/Action/Generate.php index 762dd35..5ba1065 100644 --- a/src/Migration/Action/Generate.php +++ b/src/Migration/Action/Generate.php @@ -311,13 +311,13 @@ public function getReferences(bool $skipRefSchema = false): Generator ); } - yield $constraintName => $referencesOptions + [ + yield $constraintName => array_merge($referencesOptions, [ sprintf("'referencedTable' => %s", $this->wrapWithQuotes($reference->getReferencedTable())), "'columns' => [" . join(',', array_unique($referenceColumns)) . "]", "'referencedColumns' => [" . join(',', array_unique($referencedColumns)) . "]", sprintf("'onUpdate' => '%s'", $reference->getOnUpdate()), sprintf("'onDelete' => '%s'", $reference->getOnDelete()), - ]; + ]); } } diff --git a/src/Migrations.php b/src/Migrations.php index 2ac2260..83daed7 100644 --- a/src/Migrations.php +++ b/src/Migrations.php @@ -28,6 +28,7 @@ use Phalcon\Migrations\Version\IncrementalItem; use Phalcon\Migrations\Version\ItemCollection as VersionCollection; use Phalcon\Migrations\Version\TimestampedItem; +use SplFileInfo; class Migrations { @@ -191,6 +192,7 @@ public static function generate(array $options) * @param array $options * @throws DbException * @throws RuntimeException + * @throws Exception */ public static function run(array $options) { @@ -198,6 +200,7 @@ public static function run(array $options) $listTables = new ListTablesIterator(); $optionStack->setOptions($options); $optionStack->setDefaultOption('verbose', false); + $optionStack->setDefaultOption('skip-foreign-checks', false); // Define versioning type to be used if (!empty($options['tsBased']) || $optionStack->getOption('tsBased')) { @@ -355,12 +358,18 @@ public static function run(array $options) $migrationStartTime = date('Y-m-d H:i:s'); if ($optionStack->getOption('tableName') === '@') { + /** @var SplFileInfo $fileInfo */ foreach ($iterator as $fileInfo) { if (!$fileInfo->isFile() || 0 !== strcasecmp($fileInfo->getExtension(), 'php')) { continue; } - ModelMigration::migrate($fileInfo->getBasename('.php'), $initialVersion, $versionItem); + ModelMigration::migrate( + $fileInfo->getBasename('.php'), + $initialVersion, + $versionItem, + $optionStack->getOption('skip-foreign-checks') + ); } } else { if (!empty($prefix)) { @@ -369,7 +378,12 @@ public static function run(array $options) $tables = explode(',', $optionStack->getOption('tableName')); foreach ($tables as $tableName) { - ModelMigration::migrate($tableName, $initialVersion, $versionItem); + ModelMigration::migrate( + $tableName, + $initialVersion, + $versionItem, + $optionStack->getOption('skip-foreign-checks') + ); } } diff --git a/src/Mvc/Model/Migration.php b/src/Mvc/Model/Migration.php index c7564d1..f969cdf 100644 --- a/src/Mvc/Model/Migration.php +++ b/src/Mvc/Model/Migration.php @@ -328,12 +328,14 @@ public static function shouldExportDataFromTable(string $table, array $exportTab * @param string $tableName * @param ItemInterface|null $fromVersion * @param ItemInterface|null $toVersion + * @param bool $skipForeignChecks * @throws Exception */ public static function migrate( string $tableName, ItemInterface $fromVersion = null, - ItemInterface $toVersion = null + ItemInterface $toVersion = null, + bool $skipForeignChecks = false ): void { $fromVersion = $fromVersion ?: VersionCollection::createItem($fromVersion); $toVersion = $toVersion ?: VersionCollection::createItem($toVersion); @@ -342,6 +344,10 @@ public static function migrate( return; // nothing to do } + if ($skipForeignChecks === true) { + self::$connection->execute('SET FOREIGN_KEY_CHECKS=0'); + } + if ($fromVersion->getStamp() < $toVersion->getStamp()) { $toMigration = self::createClass($toVersion, $tableName); @@ -378,6 +384,10 @@ public static function migrate( $toMigration->morph(); } } + + if ($skipForeignChecks === true) { + self::$connection->execute('SET FOREIGN_KEY_CHECKS=1'); + } } /** diff --git a/src/Version/IncrementalItem.php b/src/Version/IncrementalItem.php index b073481..1fff9f2 100644 --- a/src/Version/IncrementalItem.php +++ b/src/Version/IncrementalItem.php @@ -135,8 +135,8 @@ public static function between($initialVersion, $finalVersion, $versions) foreach ($versions as $version) { /** @var ItemInterface $version */ if ( - ($version->getStamp() >= $initialVersion->getStamp()) - && ($version->getStamp() <= $finalVersion->getStamp()) + $version->getStamp() >= $initialVersion->getStamp() && + $version->getStamp() <= $finalVersion->getStamp() ) { $betweenVersions[] = $version; } @@ -149,7 +149,7 @@ public static function between($initialVersion, $finalVersion, $versions) * @param ItemInterface[] $versions * @return array ItemInterface[] */ - public static function sortAsc($versions) + public static function sortAsc(array $versions): array { $sortData = []; foreach ($versions as $version) { diff --git a/tests/_support/Helper/Mysql.php b/tests/_support/Helper/Mysql.php index 03b9cb9..71d8fc9 100644 --- a/tests/_support/Helper/Mysql.php +++ b/tests/_support/Helper/Mysql.php @@ -31,17 +31,23 @@ public function _initialize() public function _before(TestInterface $test) { + $this->setForeignKeys(); foreach ($this->getPhalconDb()->listTables() as $table) { $this->getPhalconDb()->dropTable($table); } + + $this->setForeignKeys(true); } public function _after(TestInterface $test) { + $this->setForeignKeys(); foreach ($this->getPhalconDb()->listTables() as $table) { $this->getPhalconDb()->dropTable($table); } + $this->setForeignKeys(true); + /** * Reset filename or DB connection */ @@ -123,4 +129,14 @@ public function batchInsert(string $table, array $columns, array $rows) $this->getPhalconDb()->execute($query); } + + /** + * Executes 'SET FOREIGN_KEY_CHECKS' query + * + * @param bool $enabled + */ + protected function setForeignKeys(bool $enabled = false): void + { + $this->getPhalconDb()->execute('SET FOREIGN_KEY_CHECKS=' . intval($enabled)); + } } diff --git a/tests/cli/GenerateCest.php b/tests/cli/GenerateCest.php index 9e1d4d5..8130378 100644 --- a/tests/cli/GenerateCest.php +++ b/tests/cli/GenerateCest.php @@ -87,6 +87,7 @@ public function generateWithSkipRefSchema(CliTester $I): void $content = file_get_contents(codecept_output_dir('1.0.0/cli-skip-ref-schema.php')); $I->assertFalse(strpos($content, "'referencedSchema' => '$schema',")); + $I->assertNotFalse(strpos($content, "'referencedTable' => 'client',")); } /** @@ -107,6 +108,7 @@ public function generateWithRefSchema(CliTester $I): void $content = file_get_contents(codecept_output_dir('1.0.0/cli-skip-ref-schema.php')); $I->assertNotFalse(strpos($content, "'referencedSchema' => '$schema',")); + $I->assertNotFalse(strpos($content, "'referencedTable' => 'client',")); } /** diff --git a/tests/cli/RunCest.php b/tests/cli/RunCest.php index 7499966..3afaa6f 100644 --- a/tests/cli/RunCest.php +++ b/tests/cli/RunCest.php @@ -15,9 +15,15 @@ use CliTester; use Phalcon\Db\Column; +use Phalcon\Db\Reference; final class RunCest { + /** + * @var string + */ + protected $configPath = 'tests/_data/cli/migrations.php'; + /** * @param CliTester $I */ @@ -46,14 +52,113 @@ public function generateAndRun(CliTester $I): void ], ]); - $configPath = 'tests/_data/cli/migrations.php'; + $I->runShellCommand('php phalcon-migrations generate --config=' . $this->configPath); + $I->seeInShellOutput('Success: Version 1.0.0 was successfully generated'); + $I->seeResultCodeIs(0); + + $I->runShellCommand('php phalcon-migrations run --config=' . $this->configPath); + $I->seeInShellOutput('Success: Version 1.0.0 was successfully migrated'); + $I->seeResultCodeIs(0); + } + + /** + * @param CliTester $I + */ + public function skipForeignKeys(CliTester $I): void + { + $table1 = 'client'; + $table2 = 'x-skip-foreign-keys'; + + $this->createTablesWithForeignKey($I, $table1, $table2); - $I->runShellCommand('php phalcon-migrations generate --config=' . $configPath); + $I->runShellCommand('php phalcon-migrations generate --config=' . $this->configPath); $I->seeInShellOutput('Success: Version 1.0.0 was successfully generated'); $I->seeResultCodeIs(0); - $I->runShellCommand('php phalcon-migrations run --config=' . $configPath); + $migrationContent = file_get_contents(codecept_output_dir('1.0.0/' . $table2 . '.php')); + $I->assertNotFalse(strpos($migrationContent, "'referencedTable' => 'client',")); + + $I->getPhalconDb()->dropTable($table2); + $I->getPhalconDb()->dropTable($table1); + + $I->runShellCommand('php phalcon-migrations run --skip-foreign-checks --config=' . $this->configPath); $I->seeInShellOutput('Success: Version 1.0.0 was successfully migrated'); $I->seeResultCodeIs(0); } + + /** + * @param CliTester $I + */ + public function expectForeignKeyDbError(CliTester $I): void + { + $table1 = 'z-client'; + $table2 = 'skip-foreign-keys'; + + $this->createTablesWithForeignKey($I, $table1, $table2); + + $I->runShellCommand('php phalcon-migrations generate --config=' . $this->configPath); + $I->seeInShellOutput('Success: Version 1.0.0 was successfully generated'); + $I->seeResultCodeIs(0); + + $migrationContent = file_get_contents(codecept_output_dir('1.0.0/' . $table2 . '.php')); + $I->assertNotFalse(strpos($migrationContent, "'referencedTable' => '" . $table1 . "',")); + + $I->getPhalconDb()->dropTable($table2); + $I->getPhalconDb()->dropTable($table1); + + $I->runShellCommand('php phalcon-migrations run --config=' . $this->configPath, false); + $I->seeInShellOutput('Fatal Error: SQLSTATE[HY000]: General error: 1215 Cannot add foreign key constraint'); + $I->seeResultCodeIs(1); + } + + /** + * DRY! + * + * @param CliTester $I + * @param string $table1 + * @param string $table2 + */ + protected function createTablesWithForeignKey(CliTester $I, string $table1, string $table2): void + { + $schema = getenv('MYSQL_TEST_DB_DATABASE'); + + $I->getPhalconDb()->createTable($table1, $schema, [ + 'columns' => [ + new Column('id', [ + 'type' => Column::TYPE_INTEGER, + 'size' => 11, + 'notNull' => true, + 'primary' => true, + ]), + ], + ]); + + $I->getPhalconDb()->createTable($table2, $schema, [ + 'columns' => [ + new Column('id', [ + 'type' => Column::TYPE_INTEGER, + 'size' => 10, + 'notNull' => true, + ]), + new Column('clientId', [ + 'type' => Column::TYPE_INTEGER, + 'size' => 11, + 'notNull' => true, + ]), + ], + 'references' => [ + new Reference( + 'fk_client_1', + [ + 'referencedSchema' => $schema, + 'referencedTable' => $table1, + 'columns' => ['clientId'], + 'referencedColumns' => ['id'], + 'onUpdate' => 'NO ACTION', + 'onDelete' => 'NO ACTION', + ] + ), + ], + ]); + } } diff --git a/tests/integration/Migration/Action/GenerateCest.php b/tests/integration/Migration/Action/GenerateCest.php index 2d092d4..a927d3e 100644 --- a/tests/integration/Migration/Action/GenerateCest.php +++ b/tests/integration/Migration/Action/GenerateCest.php @@ -41,6 +41,9 @@ public function construct(IntegrationTester $I): void $I->assertNull($class->getPrimaryColumnName()); } + /** + * @param IntegrationTester $I + */ public function getReferences(IntegrationTester $I): void { $I->wantToTest('Migration\Action\Generate - getReferences()'); @@ -69,6 +72,9 @@ public function getReferences(IntegrationTester $I): void $I->assertNotFalse(array_search("'referencedSchema' => 'public'", current($generatedReferences))); } + /** + * @param IntegrationTester $I + */ public function getReferencesWithoutSchema(IntegrationTester $I): void { $I->wantToTest('Migration\Action\Generate - getReferences() without schema'); @@ -140,6 +146,9 @@ public function getReferencesWithoutSchema(IntegrationTester $I): void $I->assertFalse($schemaFound2); } + /** + * @param IntegrationTester $I + */ public function getReferencesWithSchema(IntegrationTester $I): void { $I->wantToTest('Migration\Action\Generate - getReferences() with schema'); diff --git a/tests/mysql.suite.yml b/tests/mysql.suite.yml index 02f2cd4..05106ba 100644 --- a/tests/mysql.suite.yml +++ b/tests/mysql.suite.yml @@ -14,5 +14,7 @@ modules: initial_queries: - "SET NAMES utf8;" - "CREATE DATABASE IF NOT EXISTS `%MYSQL_TEST_DB_DATABASE%`" + - "SET GLOBAL sql_mode='ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION'" + - "SET GLOBAL FOREIGN_KEY_CHECKS=1" - "USE `%MYSQL_TEST_DB_DATABASE%`" step_decorators: ~