diff --git a/Migrate/Command/AbstractEnvCommand.php b/Migrate/Command/AbstractEnvCommand.php index fec017a..5c8284f 100644 --- a/Migrate/Command/AbstractEnvCommand.php +++ b/Migrate/Command/AbstractEnvCommand.php @@ -11,6 +11,7 @@ use Migrate\Migration; use Migrate\Utils\ArrayUtil; +use SebastianBergmann\GlobalState\RuntimeException; use Symfony\Component\Config\FileLocator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -52,6 +53,13 @@ public function getChangelogTable() return ArrayUtil::get($this->getConfig(), 'changelog'); } + protected function checkEnv() + { + if (!file_exists(getcwd() . '/.php-database-migration/environments')) { + throw new \RuntimeException("you are not in an initialized php-database-migration directory"); + } + } + protected function init(InputInterface $input, OutputInterface $output, $env = null) { $configDirectory = array(getcwd() . '/.php-database-migration/environments'); @@ -100,7 +108,7 @@ protected function init(InputInterface $input, OutputInterface $output, $env = n public function getLocalMigrations() { $fileList = scandir($this->getMigrationDir()); - $fileList = ArrayUtil::filter($fileList); + $fileList = ArrayUtil::filter($this->getMigrationDir(), $fileList); $migrations = array(); foreach ($fileList as $file) { @@ -204,8 +212,22 @@ public function removeFromChangelog(Migration $migration) */ public function executeUpMigration(Migration $migration) { - $this->getDb()->query($migration->getSqlUp()); + $this->getDb()->beginTransaction(); + $result = $this->getDb()->exec($migration->getSqlUp()); + + if ($result === false) { + // error while executing the migration + $errorInfo = ""; + $errorInfos = $this->getDb()->errorInfo(); + foreach ($errorInfos as $line) { + $errorInfo .= "\n$line"; + } + $this->getDb()->rollBack(); + throw new \RuntimeException("migration error, some SQL may be wrong\n\nid: {$migration->getId()}\nfile: {$migration->getFile()}\n" . $errorInfo); + } + $this->saveToChangelog($migration); + $this->getDb()->commit(); } /** @@ -213,8 +235,21 @@ public function executeUpMigration(Migration $migration) */ public function executeDownMigration(Migration $migration) { - $this->getDb()->query($migration->getSqlDown()); + $this->getDb()->beginTransaction(); + $result = $this->getDb()->exec($migration->getSqlDown()); + + if ($result === false) { + // error while executing the migration + $errorInfo = ""; + $errorInfos = $this->getDb()->errorInfo(); + foreach ($errorInfos as $line) { + $errorInfo .= "\n$line"; + } + $this->getDb()->rollBack(); + throw new \RuntimeException("migration error, some SQL may be wrong\n\nid: {$migration->getId()}\nfile: {$migration->getFile()}\n" . $errorInfo); + } $this->removeFromChangelog($migration); + $this->getDb()->commit(); } protected function filterMigrationsToExecute(InputInterface $input, OutputInterface $output) diff --git a/Migrate/Command/CreateCommand.php b/Migrate/Command/CreateCommand.php index cd823cc..d8849b1 100644 --- a/Migrate/Command/CreateCommand.php +++ b/Migrate/Command/CreateCommand.php @@ -28,6 +28,8 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { + $this->checkEnv(); + /* @var $questions QuestionHelper */ $questions = $this->getHelperSet()->get('question'); diff --git a/Migrate/Command/DownCommand.php b/Migrate/Command/DownCommand.php index 8d1e815..8b00235 100644 --- a/Migrate/Command/DownCommand.php +++ b/Migrate/Command/DownCommand.php @@ -46,6 +46,8 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { + $this->checkEnv(); + $this->init($input, $output); /* @var $questions QuestionHelper */ diff --git a/Migrate/Command/UpCommand.php b/Migrate/Command/UpCommand.php index 561a015..40804da 100644 --- a/Migrate/Command/UpCommand.php +++ b/Migrate/Command/UpCommand.php @@ -43,6 +43,8 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { + $this->checkEnv(); + $this->init($input, $output); $toExecute = $this->filterMigrationsToExecute($input, $output); diff --git a/Migrate/Migration.php b/Migrate/Migration.php index d9736c0..ecaec53 100644 --- a/Migrate/Migration.php +++ b/Migrate/Migration.php @@ -117,6 +117,8 @@ public function setSqlUp($sqlUp) $this->sqlUp = $sqlUp; } + + /** * @return mixed */ @@ -180,8 +182,41 @@ public function load($migrationDir) @$content = file_get_contents($migrationDir . '/' . $this->getFile()); if ($content && strpos($content, '@UNDO') > 0) { $sql = explode('-- @UNDO', $content); - $this->setSqlUp($sql[0]); - $this->setSqlDown($sql[1]); + $this->setSqlUp($this->parseSQLFile($migrationDir, $sql[0])); + $this->setSqlDown($this->parseSQLFile($migrationDir, $sql[1])); + } + } + + public function parseSQLFile($migrationDir, $content = null, $sqlFile = null, $nestingLevel = 1) { + if($nestingLevel > 5) { + // Do not allow more than 5 nesting level + throw new \RuntimeException(sprintf('Local migration "%s" is invalid : the "-- @FILE" annotation does not support more than 5 nesting level !', $this->getDescription())); + } + if( $sqlFile != null ) { + @$content = file_get_contents($migrationDir . '/' . $sqlFile); + } + if($content != null) { + if( strpos($content, '-- @FILE') !== false ) { + $buffer = ''; + $isFile = false; + $lines = explode("\n", $content); + foreach($lines as $line) { + if( strpos($line, '-- @FILE') !== false ) { + $isFile = true; + continue; + } + if( $isFile ) { + $filename = trim($line); + $buffer .= PHP_EOL . $this->parseSQLFile($migrationDir, null, $filename, ($nestingLevel+1)); + $isFile = false; + continue; + } + $buffer .= PHP_EOL . $line; + } + return $buffer; + } else { + return $content; + } } } } \ No newline at end of file diff --git a/Migrate/Utils/ArrayUtil.php b/Migrate/Utils/ArrayUtil.php index 9fa7332..b4278e4 100644 --- a/Migrate/Utils/ArrayUtil.php +++ b/Migrate/Utils/ArrayUtil.php @@ -14,18 +14,14 @@ public static function get(array $array, $key) { return (array_key_exists($key, $array)) ? $array[$key] : null; } - public static function filter(array $array) { - if (isset($array['.'])) { - unset($array['.']); + public static function filter($dir, array $array) { + $files = array(); + foreach ($array as $file) { + if(!is_dir(sprintf('%s/%s', $dir, $file))){ + $files[] = $file; + } } - if (isset($array['..'])) { - unset($array['..']); - } - - unset($array[0]); - unset($array[1]); - - return $array; + return $files; } } \ No newline at end of file diff --git a/composer.json b/composer.json index 55bd86e..529ce7c 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { - "name": "alwex/php-database-migration", + "name": "php-database-migration/php-database-migration", "description": "rake and mybatis SQL migration tool", - "homepage": "https://github.com/alwex/php-database-migration", + "homepage": "https://github.com/tellim/php-database-migration", "keywords": ["rake", "mybatis", "migration", "sql"], "type": "library", "license": "MIT", diff --git a/tests/Command/CreateCommandTest.php b/tests/Command/CreateCommandTest.php index 2fd2825..ab95850 100644 --- a/tests/Command/CreateCommandTest.php +++ b/tests/Command/CreateCommandTest.php @@ -58,4 +58,21 @@ public function testExecute() $this->assertEquals($expected, $content); } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage you are not in an initialized php-database-migration directory + */ + public function testCreateNotInAnInitialisedProject() + { + $this->cleanEnv(); + + $application = new Application(); + $application->add(new CreateCommand()); + + $command = $application->find('migrate:create'); + $commandTester = new CommandTester($command); + + $commandTester->execute(array('command' => $command->getName())); + } } \ No newline at end of file diff --git a/tests/Command/UpDownCommandTest.php b/tests/Command/UpDownCommandTest.php index 62d7649..bff6771 100644 --- a/tests/Command/UpDownCommandTest.php +++ b/tests/Command/UpDownCommandTest.php @@ -28,7 +28,7 @@ public function setUp() $this->initEnv(); $this->createMigration('0', "CREATE TABLE test (id INTEGER, thevalue TEXT);", "DROP TABLE test;"); - $this->createMigration('1', "INSERT INTO test VALUES (1, 'one');", "DELETE FROM test WHERE id = 1;"); + $this->createMigration('1', "SELECT 1", "DELETE FROM test WHERE id = 1;"); $this->createMigration('2', "INSERT INTO test VALUES (2, 'two');", "DELETE FROM test WHERE id = 2;"); self::$application = new Application(); @@ -42,6 +42,50 @@ public function tearDown() $this->cleanEnv(); } + /** + * @expectedException \RuntimeException + */ + public function testUpMigrationWithError() + { + $this->createMigration('3', "SELECT ;", "SELECT ;"); + $command = self::$application->find('migrate:up'); + $commandTester = new CommandTester($command); + + $commandTester->execute(array( + 'command' => $command->getName(), + 'env' => 'testing' + )); + } + + /** + * @expectedException \RuntimeException + */ + public function testDownMigrationWithError() + { + $this->createMigration('3', "SELECT 1;", "SELECT ;"); + + + $command = self::$application->find('migrate:up'); + $commandTester = new CommandTester($command); + + $commandTester->execute(array( + 'command' => $command->getName(), + 'env' => 'testing' + )); + + $command = self::$application->find('migrate:down'); + $commandTester = new CommandTester($command); + + /* @var $question QuestionHelper */ + $question = $command->getHelper('question'); + $question->setInputStream(InputStreamUtil::type("yes\n")); + + $commandTester->execute(array( + 'command' => $command->getName(), + 'env' => 'testing' + )); + } + public function testUpAllPendingMigrations() { @@ -308,4 +352,56 @@ public function testDownTo() $this->assertEquals($expected, $commandTester->getDisplay()); } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage you are not in an initialized php-database-migration directory + */ + public function testUpInANotInitializedDirectory() + { + $this->cleanEnv(); + + $command = self::$application->find('migrate:up'); + $commandTester = new CommandTester($command); + + $commandTester->execute(array( + 'command' => $command->getName(), + 'env' => 'testing', + )); + + $command = self::$application->find('migrate:down'); + $commandTester = new CommandTester($command); + + $commandTester->execute(array( + 'command' => $command->getName(), + 'env' => 'testing', + '--to' => '1' + )); + } + + /** + * @expectedException \RuntimeException + * @expectedExceptionMessage you are not in an initialized php-database-migration directory + */ + public function testDownInANotInitializedDirectory() + { + $this->cleanEnv(); + + $command = self::$application->find('migrate:down'); + $commandTester = new CommandTester($command); + + $commandTester->execute(array( + 'command' => $command->getName(), + 'env' => 'testing', + )); + + $command = self::$application->find('migrate:down'); + $commandTester = new CommandTester($command); + + $commandTester->execute(array( + 'command' => $command->getName(), + 'env' => 'testing', + '--to' => '1' + )); + } } \ No newline at end of file diff --git a/tests/MigrationTest.php b/tests/MigrationTest.php new file mode 100644 index 0000000..65f0de4 --- /dev/null +++ b/tests/MigrationTest.php @@ -0,0 +1,63 @@ +migration = new Migration(); + } + + + public function testLoad() { + + $this->migration->setFile('rollout.sql'); + $this->migration->load(__DIR__.'/data/feature-1/'); + + $expected =<<assertEquals($expected, $this->migration->getSqlUp() ); + + $expected =<<assertEquals($expected, $this->migration->getSqlDown() ); + + } + + /** + * @expectedException Exception + * @expectedExceptionMessageRegExp #^.* the "-- @FILE" annotation does not support more than 5 nesting level !# + */ + public function testInfiniteLoop() + { + + $this->migration->setFile('rollout.sql'); + $this->migration->load(__DIR__ . '/data/feature-infinite-loop/'); + } +} \ No newline at end of file diff --git a/tests/data/feature-1/down/db1/table1.sql b/tests/data/feature-1/down/db1/table1.sql new file mode 100644 index 0000000..0992a1c --- /dev/null +++ b/tests/data/feature-1/down/db1/table1.sql @@ -0,0 +1 @@ +update db1.table1 set foo = 'foo'; \ No newline at end of file diff --git a/tests/data/feature-1/down/db1/table2.sql b/tests/data/feature-1/down/db1/table2.sql new file mode 100644 index 0000000..12daa8e --- /dev/null +++ b/tests/data/feature-1/down/db1/table2.sql @@ -0,0 +1 @@ +update db1.table2 set foo = 'foo'; \ No newline at end of file diff --git a/tests/data/feature-1/down/db2/table1.sql b/tests/data/feature-1/down/db2/table1.sql new file mode 100644 index 0000000..2863b48 --- /dev/null +++ b/tests/data/feature-1/down/db2/table1.sql @@ -0,0 +1 @@ +update db2.table1 set foo = 'foo'; \ No newline at end of file diff --git a/tests/data/feature-1/rollout.sql b/tests/data/feature-1/rollout.sql new file mode 100644 index 0000000..bd4b2bb --- /dev/null +++ b/tests/data/feature-1/rollout.sql @@ -0,0 +1,17 @@ +-- Test that file inclusion works correctly +-- Migration SQL that makes the change goes here. +-- @FILE +up/db1/table1.sql +-- @FILE +up/db1/table2.sql +-- @FILE +up/db2/table1.sql + +-- @UNDO +-- SQL to undo the change goes here. +-- @FILE +down/db1/table1.sql +-- @FILE +down/db1/table2.sql +-- @FILE +down/db2/table1.sql \ No newline at end of file diff --git a/tests/data/feature-1/up/db1/table1.sql b/tests/data/feature-1/up/db1/table1.sql new file mode 100644 index 0000000..f1eb1e5 --- /dev/null +++ b/tests/data/feature-1/up/db1/table1.sql @@ -0,0 +1 @@ +update db1.table1 set foo = 'bar'; \ No newline at end of file diff --git a/tests/data/feature-1/up/db1/table2.sql b/tests/data/feature-1/up/db1/table2.sql new file mode 100644 index 0000000..66ca31e --- /dev/null +++ b/tests/data/feature-1/up/db1/table2.sql @@ -0,0 +1 @@ +update db1.table2 set foo = 'bar'; \ No newline at end of file diff --git a/tests/data/feature-1/up/db2/table1.sql b/tests/data/feature-1/up/db2/table1.sql new file mode 100644 index 0000000..b4427a5 --- /dev/null +++ b/tests/data/feature-1/up/db2/table1.sql @@ -0,0 +1 @@ +update db2.table1 set foo = 'bar'; \ No newline at end of file diff --git a/tests/data/feature-infinite-loop/rollout.sql b/tests/data/feature-infinite-loop/rollout.sql new file mode 100644 index 0000000..8175771 --- /dev/null +++ b/tests/data/feature-infinite-loop/rollout.sql @@ -0,0 +1,8 @@ +-- Test that file inclusion can't loop infinitely +-- Migration SQL that makes the change goes here. +-- @FILE +up/db1/table1.sql + + +-- @UNDO +-- SQL to undo the change goes here. diff --git a/tests/data/feature-infinite-loop/up/db1/table1.sql b/tests/data/feature-infinite-loop/up/db1/table1.sql new file mode 100644 index 0000000..f1e84b0 --- /dev/null +++ b/tests/data/feature-infinite-loop/up/db1/table1.sql @@ -0,0 +1,2 @@ +-- @FILE +up/db1/table1.sql \ No newline at end of file