diff --git a/scripts/Phalcon/Commands/Builtin/Migration.php b/scripts/Phalcon/Commands/Builtin/Migration.php index 4ae7e76e5..9bb46a6fd 100644 --- a/scripts/Phalcon/Commands/Builtin/Migration.php +++ b/scripts/Phalcon/Commands/Builtin/Migration.php @@ -47,7 +47,7 @@ public function getPossibleParams() 'config=s' => 'Configuration file', 'migrations=s' => 'Migrations directory', 'directory=s' => 'Directory where the project was created', - 'table=s' => 'Table to migrate. Default: all', + 'table=s' => 'Table to migrate. Table name or table prefix with asterisk. Default: all', 'version=s' => 'Version to migrate', 'descr=s' => 'Migration description (used for timestamp based migration)', 'data=s' => 'Export data [always|oncreate] (Import data when run migration)', diff --git a/scripts/Phalcon/Console/OptionParserTrait.php b/scripts/Phalcon/Console/OptionParserTrait.php index 14808739e..7f7d62bcd 100644 --- a/scripts/Phalcon/Console/OptionParserTrait.php +++ b/scripts/Phalcon/Console/OptionParserTrait.php @@ -31,14 +31,19 @@ trait OptionParserTrait { /** - * Indicates whether the script was a particular option. + * Get prefix from the option * - * @param string $key - * @param array $option - * @return boolean + * @param string $prefix + * @param mixed $prefixEnd + * + * @return mixed */ - public function isReceivedOption($key, array $options) + public function getPrefixOption($prefix, $prefixEnd = '*') { - return isset($options[$key]); + if (substr($prefix, -1) != $prefixEnd) { + return ''; + } + + return substr($prefix, 0, -1); } } diff --git a/scripts/Phalcon/Console/OptionStack.php b/scripts/Phalcon/Console/OptionStack.php index d1bb041d4..e22be0e5a 100644 --- a/scripts/Phalcon/Console/OptionStack.php +++ b/scripts/Phalcon/Console/OptionStack.php @@ -19,6 +19,8 @@ namespace Phalcon\Console; +use Phalcon\Console\OptionParserTrait; + /** * Phalcon\Console\OptionStack * @@ -30,6 +32,8 @@ */ class OptionStack { + use OptionParserTrait; + /** * Parameters received by the script. * @var array @@ -128,4 +132,15 @@ public function countOptions() { return count($this->options); } + + /** + * Indicates whether the script was a particular option. + * + * @param string $key + * @return boolean + */ + public function isReceivedOption($key) + { + return isset($this->options[$key]); + } } diff --git a/scripts/Phalcon/Migrations.php b/scripts/Phalcon/Migrations.php index 4553ed3cd..87f74b1f8 100644 --- a/scripts/Phalcon/Migrations.php +++ b/scripts/Phalcon/Migrations.php @@ -35,6 +35,8 @@ use Phalcon\Version\IncrementalItem as IncrementalVersion; use Phalcon\Version\ItemCollection as VersionCollection; use Phalcon\Console\OptionStack; +use Phalcon\Mvc\Model\Migration\TableAware\ListTablesIterator; +use Phalcon\Mvc\Model\Migration\TableAware\ListTablesDb; /** * Migrations Class @@ -76,6 +78,8 @@ public static function isConsole() public static function generate(array $options) { $optionStack = new OptionStack(); + $modelMigration = new ModelMigration(); + $listTables = new ListTablesDb(); $optionStack->setOptions($options); $optionStack->setDefaultOption('version', null); $optionStack->setDefaultOption('descr', null); @@ -126,6 +130,16 @@ public static function generate(array $options) } } } else { + $prefix = $optionStack->getPrefixOption($optionStack->getOption('tableName')); + if (!empty($prefix)) { + $optionStack->setOption('tableName', $listTables->listTablesForPrefix($prefix)); + } + + if ($optionStack->getOption('tableName') == '') { + print Color::info('No one table is created. You should create tables first.') . PHP_EOL; + return; + } + $tables = explode(',', $optionStack->getOption('tableName')); foreach ($tables as $table) { $migration = ModelMigration::generate($versionItem, $table, $optionStack->getOption('exportData')); @@ -159,6 +173,7 @@ public static function generate(array $options) public static function run(array $options) { $optionStack = new OptionStack(); + $listTables = new ListTablesIterator(); $optionStack->setOptions($options); $optionStack->setDefaultOption('verbose', false); @@ -235,6 +250,7 @@ public static function run(array $options) // Run migration $versionsBetween = VersionCollection::between($initialVersion, $finalVersion, $versionItems); + $prefix = $optionStack->getPrefixOption($optionStack->getOption('tableName')); foreach ($versionsBetween as $versionItem) { // If we are rolling back, we skip migrating when initialVersion is the same as current @@ -256,11 +272,13 @@ public static function run(array $options) } $migrationStartTime = date("Y-m-d H:i:s"); + + // Directory depends on Forward or Back Migration + $iterator = new DirectoryIterator( + $migrationsDir . DIRECTORY_SEPARATOR . (ModelMigration::DIRECTION_BACK === $direction ? $initialVersion->getVersion() : $versionItem->getVersion()) + ); + if ($optionStack->getOption('tableName') === '@') { - // Directory depends on Forward or Back Migration - $iterator = new DirectoryIterator( - $migrationsDir . DIRECTORY_SEPARATOR . (ModelMigration::DIRECTION_BACK === $direction ? $initialVersion->getVersion() : $versionItem->getVersion()) - ); foreach ($iterator as $fileInfo) { if (!$fileInfo->isFile() || 0 !== strcasecmp($fileInfo->getExtension(), 'php')) { continue; @@ -268,7 +286,14 @@ public static function run(array $options) ModelMigration::migrate($initialVersion, $versionItem, $fileInfo->getBasename('.php')); } } else { - ModelMigration::migrate($initialVersion, $versionItem, $optionStack->getOption('tableName')); + if (!empty($prefix)) { + $optionStack->setOption('tableName', $listTables->listTablesForPrefix($prefix, $iterator)); + } + + $tables = explode(',', $optionStack->getOption('tableName')); + foreach ($tables as $tableName) { + ModelMigration::migrate($initialVersion, $versionItem, $tableName); + } } if (ModelMigration::DIRECTION_FORWARD == $direction) { diff --git a/scripts/Phalcon/Mvc/Model/Migration.php b/scripts/Phalcon/Mvc/Model/Migration.php index a3cbbaa56..6e7ef7a80 100644 --- a/scripts/Phalcon/Mvc/Model/Migration.php +++ b/scripts/Phalcon/Mvc/Model/Migration.php @@ -894,4 +894,14 @@ function ($value) { fclose($batchHandler); self::$_connection->commit(); } + + /** + * Get db connection + * + * @return \Phalcon\Db\AdapterInterface + */ + public function getConnection() + { + return self::$_connection; + } } diff --git a/scripts/Phalcon/Mvc/Model/Migration/TableAware/ListTablesDb.php b/scripts/Phalcon/Mvc/Model/Migration/TableAware/ListTablesDb.php new file mode 100644 index 000000000..3c639ebf5 --- /dev/null +++ b/scripts/Phalcon/Mvc/Model/Migration/TableAware/ListTablesDb.php @@ -0,0 +1,68 @@ + | + +------------------------------------------------------------------------+ +*/ + +namespace Phalcon\Mvc\Model\Migration\TableAware; + +use Phalcon\Mvc\Model\Migration\TableAware\ListTablesInterface; +use InvalidArgumentException; +use Phalcon\Mvc\Model\Migration as ModelMigration; +use Phalcon\Db\Exception as DbException; + +/** + * Phalcon\Mvc\Model\Migration\TableAware\ListTablesDb + * + * @package Phalcon\Mvc\Model\Migration\TableAware + */ +class ListTablesDb implements ListTablesInterface +{ + /** + * Get table names with prefix for running migration + * + * @param string $tablePrefix + * @param \DirectoryIterator $iterator + * @return string + */ + public function listTablesForPrefix($tablePrefix, \DirectoryIterator $iterator = null) + { + if (empty($tablePrefix)) { + throw new InvalidArgumentException("Parameters weren't defined in " . __METHOD__); + } + + $modelMigration = new ModelMigration(); + $connection = $modelMigration->getConnection(); + + $tablesList = $connection->listTables(); + if (empty($tablesList)) { + return ''; + } + + $strlen = strlen($tablePrefix); + foreach ($tablesList as $key => $value) { + if (substr($value, 0, $strlen) != $tablePrefix) { + unset($tablesList[$key]); + } + } + + if (empty($tablesList)) { + throw new DbException("Specified table prefix doesn't match with any table name"); + } + + return implode(',', $tablesList); + } +} diff --git a/scripts/Phalcon/Mvc/Model/Migration/TableAware/ListTablesInterface.php b/scripts/Phalcon/Mvc/Model/Migration/TableAware/ListTablesInterface.php new file mode 100644 index 000000000..84bc904c6 --- /dev/null +++ b/scripts/Phalcon/Mvc/Model/Migration/TableAware/ListTablesInterface.php @@ -0,0 +1,37 @@ + | + +------------------------------------------------------------------------+ +*/ + +namespace Phalcon\Mvc\Model\Migration\TableAware; + +/** + * Phalcon\Mvc\Model\Migration\TableAware\ListTablesInterface + * + * @package Phalcon\Mvc\Model\Migration\TableAware + */ +interface ListTablesInterface +{ + /** + * Get list table from prefix + * + * @param string $tablePrefix Table prefix + * @param \DirectoryIterator $iterator + * @return string + */ + public function listTablesForPrefix($tablePrefix, \DirectoryIterator $iterator = null); +} diff --git a/scripts/Phalcon/Mvc/Model/Migration/TableAware/ListTablesIterator.php b/scripts/Phalcon/Mvc/Model/Migration/TableAware/ListTablesIterator.php new file mode 100644 index 000000000..5d07df83f --- /dev/null +++ b/scripts/Phalcon/Mvc/Model/Migration/TableAware/ListTablesIterator.php @@ -0,0 +1,60 @@ + | + +------------------------------------------------------------------------+ +*/ + +namespace Phalcon\Mvc\Model\Migration\TableAware; + +use Phalcon\Mvc\Model\Migration\TableAware\ListTablesInterface; +use DirectoryIterator; +use InvalidArgumentException; + +/** + * Phalcon\Mvc\Model\Migration\TableAware\ListTablesIterator + * + * @package Phalcon\Mvc\Model\Migration\TableAware + */ +class ListTablesIterator implements ListTablesInterface +{ + /** + * Get table names with prefix for running migration + * + * @param string $tablePrefix + * @param DirectoryIterator $iterator + * @return string + */ + public function listTablesForPrefix($tablePrefix, DirectoryIterator $iterator = null) + { + if (empty($tablePrefix) || empty($iterator)) { + throw new InvalidArgumentException("Parameters weren't defined in " . __METHOD__); + } + + $strlen = strlen($tablePrefix); + $fileNames = []; + foreach ($iterator as $fileInfo) { + if (substr($fileInfo->getFilename(), 0, $strlen) == $tablePrefix) { + $file = explode('.', $fileInfo->getFilename()); + $fileNames[] = $file[0]; + } + } + + $fileNames = array_unique($fileNames); +// natsort($fileNames); + + return implode(',', $fileNames); + } +} diff --git a/tests/_data/console/app/test_table_prefix/migrations/1.0.0/issue595_1.php b/tests/_data/console/app/test_table_prefix/migrations/1.0.0/issue595_1.php new file mode 100644 index 000000000..ed23f539e --- /dev/null +++ b/tests/_data/console/app/test_table_prefix/migrations/1.0.0/issue595_1.php @@ -0,0 +1,76 @@ +morphTable('issue595_1', [ + 'columns' => [ + new Column( + 'id', + [ + 'type' => Column::TYPE_INTEGER, + 'unsigned' => true, + 'notNull' => true, + 'autoIncrement' => true, + 'size' => 10, + 'first' => true + ] + ), + new Column( + 'name', + [ + 'type' => Column::TYPE_VARCHAR, + 'notNull' => true, + 'size' => 45, + 'after' => 'id' + ] + ) + ], + 'indexes' => [ + new Index('PRIMARY', ['id'], 'PRIMARY') + ], + 'options' => [ + 'TABLE_TYPE' => 'BASE TABLE', + 'AUTO_INCREMENT' => '1', + 'ENGINE' => 'InnoDB', + 'TABLE_COLLATION' => 'utf8_general_ci' + ], + ] + ); + } + + /** + * Run the migrations + * + * @return void + */ + public function up() + { + + } + + /** + * Reverse the migrations + * + * @return void + */ + public function down() + { + + } + +} diff --git a/tests/_data/console/app/test_table_prefix/migrations/1.0.0/issue595_2.php b/tests/_data/console/app/test_table_prefix/migrations/1.0.0/issue595_2.php new file mode 100644 index 000000000..1ab6a29db --- /dev/null +++ b/tests/_data/console/app/test_table_prefix/migrations/1.0.0/issue595_2.php @@ -0,0 +1,76 @@ +morphTable('issue595_2', [ + 'columns' => [ + new Column( + 'id', + [ + 'type' => Column::TYPE_INTEGER, + 'unsigned' => true, + 'notNull' => true, + 'autoIncrement' => true, + 'size' => 10, + 'first' => true + ] + ), + new Column( + 'version', + [ + 'type' => Column::TYPE_INTEGER, + 'notNull' => true, + 'size' => 45, + 'after' => 'id' + ] + ) + ], + 'indexes' => [ + new Index('PRIMARY', ['id'], 'PRIMARY') + ], + 'options' => [ + 'TABLE_TYPE' => 'BASE TABLE', + 'AUTO_INCREMENT' => '1', + 'ENGINE' => 'InnoDB', + 'TABLE_COLLATION' => 'utf8_general_ci' + ], + ] + ); + } + + /** + * Run the migrations + * + * @return void + */ + public function up() + { + + } + + /** + * Reverse the migrations + * + * @return void + */ + public function down() + { + + } + +} diff --git a/tests/_data/schemas/mysql/dump.sql b/tests/_data/schemas/mysql/dump.sql index 65cb1a9a4..10aa3c96d 100644 --- a/tests/_data/schemas/mysql/dump.sql +++ b/tests/_data/schemas/mysql/dump.sql @@ -42,3 +42,22 @@ CREATE TABLE `test_dry_verbose` ( `active` tinyint(1) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- +-- Table structure for testing table prefix, issue #595 +-- +DROP TABLE IF EXISTS `issue595_1`; +CREATE TABLE `issue595_1` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(45) NOT NULL, + `version` int(45) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +DROP TABLE IF EXISTS `issue595_2`; +CREATE TABLE `issue595_2` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `version` int(45) NOT NULL, + `name` varchar(45) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/tests/console/GenerateTablePrefixMigrationCept.php b/tests/console/GenerateTablePrefixMigrationCept.php new file mode 100644 index 000000000..05367db4e --- /dev/null +++ b/tests/console/GenerateTablePrefixMigrationCept.php @@ -0,0 +1,20 @@ +wantToTest('Generating migration for testing table prefix'); + +$I->amInPath(dirname(app_path())); + +$I->dontSeeFileFound(app_path('test_table_prefix/migrations/1.0.1/issue595_1.php')); +$I->dontSeeFileFound(app_path('test_table_prefix/migrations/1.0.1/issue595_2.php')); + +$I->runShellCommand('phalcon migration --action=generate --config=app/mysql/config.php --migrations=app/test_table_prefix/migrations --table=issue595*'); +$I->seeInShellOutput('Success: Version 1.0.1 was successfully generated'); + +$I->seeFileFound(app_path('test_table_prefix/migrations/1.0.1/issue595_1.php')); +$I->seeFileFound(app_path('test_table_prefix/migrations/1.0.1/issue595_2.php')); diff --git a/tests/console/RunTablePrefixMigrationCept.php b/tests/console/RunTablePrefixMigrationCept.php new file mode 100644 index 000000000..58e99bbff --- /dev/null +++ b/tests/console/RunTablePrefixMigrationCept.php @@ -0,0 +1,26 @@ +wantToTest('Running migration for testing table prefix'); + +$I->amInPath(dirname(app_path())); +$I->cleanDir(tests_path('_data/console/.phalcon')); + +$I->seeFileFound(app_path('test_table_prefix/migrations/1.0.1/issue595_1.php')); +$I->seeFileFound(app_path('test_table_prefix/migrations/1.0.1/issue595_2.php')); + +$I->runShellCommand('phalcon migration --action=run --config=app/mysql/config.php --migrations=app/test_table_prefix/migrations/ --version=1.0.0 --table=issue595*'); +$I->seeInShellOutput('Success: Version 1.0.0 was successfully migrated'); + +$I->runShellCommand('phalcon migration --action=run --config=app/mysql/config.php --migrations=app/test_table_prefix/migrations/ --version=1.0.1 --table=issue595*'); +$I->seeInShellOutput('Success: Version 1.0.1 was successfully migrated'); + +$I->deleteDir(app_path('test_table_prefix/migrations/1.0.1/')); +$I->dontSeeFileFound(app_path('test_table_prefix/migrations/1.0.1/issue595_1.php')); +$I->dontSeeFileFound(app_path('test_table_prefix/migrations/1.0.1/issue595_2.php')); +$I->cleanDir(tests_path('_data/console/.phalcon')); diff --git a/tests/unit/Console/OptionStackTest.php b/tests/unit/Console/OptionStackTest.php index bb037e370..70dcf9c09 100644 --- a/tests/unit/Console/OptionStackTest.php +++ b/tests/unit/Console/OptionStackTest.php @@ -114,7 +114,7 @@ function($key, $defaultValue, $expected) } /** - * Tests OptionParserTrait::isReceivedOption + * Tests OptionStack::isReceivedOption * * @test * @author Sergii Svyrydenko @@ -132,8 +132,8 @@ function($option, $expected) }, [ 'examples' => [ - [$this->isReceivedOption('true-option', $this->options->getOptions()), true], - [$this->isReceivedOption('false-option', $this->options->getOptions()), false] + [$this->options->isReceivedOption('true-option', $this->options->getOptions()), true], + [$this->options->isReceivedOption('false-option', $this->options->getOptions()), false] ] ] ); @@ -164,4 +164,56 @@ function($option, $expected) ] ); } + + /** + * Tests OptionParserTrait::getPrefixOption + * + * @test + * @issue 595 + * @author Sergii Svyrydenko + * @since 2017-10-06 + */ + public function shouldReturnPrefixFromOptionWithoutSetPrefix() + { + $this->options->setOptions(['test' => 'foo', 'test2' => 'bar']); + + $this->specify( + 'Method' . __METHOD__ . 'does not return option prefix', + function($prefix, $expected) { + expect($this->getPrefixOption($prefix))->equals($expected); + }, + [ + 'examples' => [ + ['foo*', 'foo'], + ['bar*', 'bar'] + ] + ] + ); + } + + /** + * Tests OptionParserTrait::getPrefixOption + * + * @test + * @issue 595 + * @author Sergii Svyrydenko + * @since 2017-10-06 + */ + public function shouldReturnPrefixFromOptionWithSetPrefix() + { + $this->options->setOptions(['test' => 'foo', 'test2' => 'bar']); + + $this->specify( + 'Method' . __METHOD__ . 'does not return option prefix', + function($prefix, $prefixEnd, $expected) { + expect($this->getPrefixOption($prefix, $prefixEnd))->equals($expected); + }, + [ + 'examples' => [ + ['foo^', '^', 'foo'], + ['bar?', '?', 'bar'] + ] + ] + ); + } } diff --git a/tests/unit/Mvc/Model/Migration/TableAware/ListTablesDbTest.php b/tests/unit/Mvc/Model/Migration/TableAware/ListTablesDbTest.php new file mode 100644 index 000000000..a48fdff0c --- /dev/null +++ b/tests/unit/Mvc/Model/Migration/TableAware/ListTablesDbTest.php @@ -0,0 +1,69 @@ + | + +------------------------------------------------------------------------+ +*/ + +class ListTablesDbTest extends UnitTest +{ + public function _before() + { + parent::_before(); + + try { + $config = include(app_path('mysql/config.php')); + if (is_array($config)) { + $config = new Config($config); + } + + ModelMigration::setup($config->database); + } catch (\PDOException $e) { + throw new \PHPUnit_Framework_SkippedTestError("Unable to connect to the database: " . $e->getMessage()); + } + } + + /** + * Tests ListTablesDb::listTablesForPrefix + * + * @test + * @issue 595 + * @author Sergii Svyrydenko + * @since 2017-10-06 + */ + public function shouldReturnListTablesFromDb() + { + $listTables = new ListTablesDb(); + + $this->specify( + 'Method' . __METHOD__ . 'did not return table list', + function ($tablePrefix, $expected) use ($listTables) { + expect($listTables->listTablesForPrefix($tablePrefix))->equals($expected); + }, + [ + 'examples' => [ + ['issue595', 'issue595_1,issue595_2'] + ] + ] + ); + } +} diff --git a/tests/unit/Mvc/Model/Migration/TableAware/ListTablesIteratorTest.php b/tests/unit/Mvc/Model/Migration/TableAware/ListTablesIteratorTest.php new file mode 100644 index 000000000..d6dc00fcd --- /dev/null +++ b/tests/unit/Mvc/Model/Migration/TableAware/ListTablesIteratorTest.php @@ -0,0 +1,53 @@ + | + +------------------------------------------------------------------------+ +*/ + +class ListTablesIteratorTest extends UnitTest +{ + /** + * Tests ListTablesIterator::listTablesForPrefix + * + * @test + * @issue 595 + * @author Sergii Svyrydenko + * @since 2017-10-06 + */ + public function shouldReturnListTablesFromIterator() + { + $iterator = new DirectoryIterator(app_path('test_table_prefix/migrations/1.0.0')); + $listTables = new ListTablesIterator(); + + $this->specify( + 'Method' . __METHOD__ . 'did not return table list', + function($tablePrefix, $expected) use ($iterator, $listTables){ + expect($listTables->listTablesForPrefix($tablePrefix, $iterator))->equals($expected); + }, + [ + 'examples' => [ + ['issue', 'issue595_1,issue595_2'] + ] + ] + ); + } +}