From 7c989f802bcf81146714ba9ea59598521af2bda9 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Wed, 12 Jul 2023 16:23:30 -0500 Subject: [PATCH 1/5] Accept description in the migration files --- src/Migration.php | 2 +- tests/MigrationTest.php | 2 +- .../up/{00013.sql => 00013-description-development.sql} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename tests/dirstructure/migrations/up/{00013.sql => 00013-description-development.sql} (100%) diff --git a/src/Migration.php b/src/Migration.php index 168e782..8c87af1 100644 --- a/src/Migration.php +++ b/src/Migration.php @@ -161,7 +161,7 @@ public function getMigrationSql($version, $increment) . "/*.sql"; $result = array_filter(glob($filePattern), function ($file) use ($version) { - return preg_match("/^0*$version(-dev)?\.sql$/", basename($file)); + return preg_match("/^0*$version(-[\w\d-]*)?\.sql$/", basename($file)); }); // Valid values are 0 or 1 diff --git a/tests/MigrationTest.php b/tests/MigrationTest.php index b37640a..2d435f9 100644 --- a/tests/MigrationTest.php +++ b/tests/MigrationTest.php @@ -63,7 +63,7 @@ public function testGetMigrationSql3() public function testGetMigrationSql4() { $this->expectException(InvalidMigrationFile::class); - $this->expectExceptionMessage("version number '13'"); + $this->expectExceptionMessage("You have two files with the same version number '13'"); $this->object->getMigrationSql(13, 1); } diff --git a/tests/dirstructure/migrations/up/00013.sql b/tests/dirstructure/migrations/up/00013-description-development.sql similarity index 100% rename from tests/dirstructure/migrations/up/00013.sql rename to tests/dirstructure/migrations/up/00013-description-development.sql From 2b75aaf8e5e8cb86e772e87b247d8f9a92e28a46 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 15 Jul 2023 15:06:26 -0500 Subject: [PATCH 2/5] Address issue #49 --- README.md | 38 +++++----------- src/Migration.php | 31 ++++++++++--- tests/MigrationTest.php | 44 ++++++++++++++++--- tests/dirstructure/migrations/up/00001.sql | 2 - tests/dirstructure/migrations/up/00002.sql | 2 - tests/dirstructure/migrations/up/00003.sql | 1 - tests/dirstructure2/base.sql | 0 tests/dirstructure2/migrations/down/0000.sql | 0 tests/dirstructure2/migrations/down/00001.sql | 0 tests/dirstructure2/migrations/down/00002.sql | 0 tests/dirstructure2/migrations/up/00001.sql | 0 tests/dirstructure2/migrations/up/00002.sql | 1 + tests/dirstructure2/migrations/up/00003.sql | 0 13 files changed, 75 insertions(+), 44 deletions(-) create mode 100644 tests/dirstructure2/base.sql create mode 100644 tests/dirstructure2/migrations/down/0000.sql create mode 100644 tests/dirstructure2/migrations/down/00001.sql create mode 100644 tests/dirstructure2/migrations/down/00002.sql create mode 100644 tests/dirstructure2/migrations/up/00001.sql create mode 100644 tests/dirstructure2/migrations/up/00002.sql create mode 100644 tests/dirstructure2/migrations/up/00003.sql diff --git a/README.md b/README.md index d79f7e4..74c986b 100644 --- a/README.md +++ b/README.md @@ -205,39 +205,23 @@ $migration->getDbDriver(); To use it, please visit: -## Tips on writing SQL migrations for Postgres +### Avoiding Partial Migration -### Rely on explicit transactions +A partial migration is when the migration script is interrupted in the middle of the process due to an error or a manual interruption. -```sql --- DO -BEGIN; +The migration table will be with the status `partial up` or `partial down` and it needs to be fixed manually before be able to migrate again. -ALTER TABLE 1; -UPDATE 1; -UPDATE 2; -UPDATE 3; -ALTER TABLE 2; +To avoid this situation you can specify the migration will be run in a transactional context. +If the migration script fails, the transaction will be rolled back and the migration table will be marked as `complete` and +the version will be the immediately previous version before the script that causes the error. -COMMIT; +To enable this feature you need to call the method `withTransactionEnabled` passing `true` as parameter: - --- DON'T -ALTER TABLE 1; -UPDATE 1; -UPDATE 2; -UPDATE 3; -ALTER TABLE 2; +```php +withTransactionEnabled(true); ``` - -It is generally desirable to wrap migration scripts inside a `BEGIN; ... COMMIT;` block. -This way, if _any_ of the inner statements fail, _none_ of them are committed and the -database does not end up in an inconsistent state. - -Mind that in case of a failure `byjg/migration` will always mark the migration as `partial` -and warn you when you attempt to run it again. The difference is that with explicit -transactions you know that the database cannot be in an inconsistent state after an -unexpected failure. +## Tips on writing SQL migrations for Postgres ### On creating triggers and SQL functions diff --git a/src/Migration.php b/src/Migration.php index 8c87af1..b19a242 100644 --- a/src/Migration.php +++ b/src/Migration.php @@ -50,6 +50,8 @@ class Migration */ private $migrationTable; + private $transaction = false; + /** * Migration constructor. * @@ -69,10 +71,14 @@ public function __construct(UriInterface $uri, $folder, $requiredBase = true, $m $this->migrationTable = $migrationTable; } + public function withTransactionEnabled($enabled = true) + { + $this->transaction = $enabled; + return $this; + } + /** - * @param $scheme - * @param $className - * @return $this + * @param $class */ public static function registerDatabase($class) { @@ -327,9 +333,22 @@ protected function migrate($upVersion, $increment, $force) call_user_func_array($this->callableProgress, ['migrate', $currentVersion, $fileInfo]); } - $this->getDbCommand()->setVersion($currentVersion, Migration::VERSION_STATUS_PARTIAL . ' ' . ($increment>0 ? 'up' : 'down')); - $this->getDbCommand()->executeSql($fileInfo["content"]); - $this->getDbCommand()->setVersion($currentVersion, Migration::VERSION_STATUS_COMPLETE); + try { + if ($this->transaction) { + $this->getDbDriver()->beginTransaction(); + } + $this->getDbCommand()->setVersion($currentVersion, Migration::VERSION_STATUS_PARTIAL . ' ' . ($increment>0 ? 'up' : 'down')); + $this->getDbCommand()->executeSql($fileInfo["content"]); + $this->getDbCommand()->setVersion($currentVersion, Migration::VERSION_STATUS_COMPLETE); + if ($this->transaction) { + $this->getDbDriver()->commitTransaction(); + } + } catch (\Exception $e) { + if ($this->transaction) { + $this->getDbDriver()->rollbackTransaction(); + } + throw $e; + } $currentVersion = $currentVersion + $increment; } } diff --git a/tests/MigrationTest.php b/tests/MigrationTest.php index 2d435f9..68269ee 100644 --- a/tests/MigrationTest.php +++ b/tests/MigrationTest.php @@ -1,6 +1,7 @@ __DIR__ . '/dirstructure/migrations/up/00001.sql', "description" => "this is a test", "exists" => true, - "checksum" => "b937afa57e363c9244fa30844dd11d312694f697", - "content" => "-- @description: this is a test\n\nselect * from mysql.users;\n", + "checksum" => "55249baf6b70c1d2e9c5362de133b2371d0dc989", + "content" => "-- @description: this is a test\n", ], $this->object->getFileContent(__DIR__ . '/dirstructure/migrations/up/00001.sql') ); @@ -127,8 +128,8 @@ public function testGetFileContent_2() "file" => __DIR__ . '/dirstructure/migrations/up/00002.sql', "description" => "another test", "exists" => true, - "checksum" => "fd8ab8176291c2dcbf0d91564405e0f98f0cd77e", - "content" => "-- @description: another test\n\nselect * from dual;", + "checksum" => "f20c73a5eb4d29e2f8edae4409a2ccc2b02c6f67", + "content" => "-- @description: another test\n", ], $this->object->getFileContent(__DIR__ . '/dirstructure/migrations/up/00002.sql') ); @@ -141,10 +142,41 @@ public function testGetFileContent_3() "file" => __DIR__ . '/dirstructure/migrations/up/00003.sql', "description" => "no description provided. Pro tip: use `-- @description:` to define one.", "exists" => true, - "checksum" => "73faaa68e2f60c11e75a9ccc18528e0ffa15127a", - "content" => "select something from sometable;", + "checksum" => "da39a3ee5e6b4b0d3255bfef95601890afd80709", + "content" => "", ], $this->object->getFileContent(__DIR__ . '/dirstructure/migrations/up/00003.sql') ); } + + public function testReset() + { + $this->expectException(\PDOException::class); + Migration::registerDatabase(SqliteDatabase::class); + $this->object = new Migration(new Uri('sqlite:///tmp/test.db'), __DIR__ . '/dirstructure2'); + $this->object->reset(); + } + + public function testResetWithoutTransactionCheck() + { + try { + Migration::registerDatabase(SqliteDatabase::class); + $this->object = new Migration(new Uri('sqlite:///tmp/test.db'), __DIR__ . '/dirstructure2'); + $this->object->reset(); + } catch (\PDOException $ex) { + $this->assertEquals(["version" => '2', "status" => "partial up"], $this->object->getCurrentVersion()); + } + } + + public function testResetWithTransactionCheck() + { + try { + Migration::registerDatabase(SqliteDatabase::class); + $this->object = new Migration(new Uri('sqlite:///tmp/test.db'), __DIR__ . '/dirstructure2'); + $this->object->withTransactionEnabled(); + $this->object->reset(); + } catch (\PDOException $ex) { + $this->assertEquals(["version" => '1', "status" => "complete"], $this->object->getCurrentVersion()); + } + } } diff --git a/tests/dirstructure/migrations/up/00001.sql b/tests/dirstructure/migrations/up/00001.sql index bba8da9..16d573f 100644 --- a/tests/dirstructure/migrations/up/00001.sql +++ b/tests/dirstructure/migrations/up/00001.sql @@ -1,3 +1 @@ -- @description: this is a test - -select * from mysql.users; diff --git a/tests/dirstructure/migrations/up/00002.sql b/tests/dirstructure/migrations/up/00002.sql index 3f8ff69..d4f1bbe 100644 --- a/tests/dirstructure/migrations/up/00002.sql +++ b/tests/dirstructure/migrations/up/00002.sql @@ -1,3 +1 @@ -- @description: another test - -select * from dual; \ No newline at end of file diff --git a/tests/dirstructure/migrations/up/00003.sql b/tests/dirstructure/migrations/up/00003.sql index 3f17ec4..e69de29 100644 --- a/tests/dirstructure/migrations/up/00003.sql +++ b/tests/dirstructure/migrations/up/00003.sql @@ -1 +0,0 @@ -select something from sometable; \ No newline at end of file diff --git a/tests/dirstructure2/base.sql b/tests/dirstructure2/base.sql new file mode 100644 index 0000000..e69de29 diff --git a/tests/dirstructure2/migrations/down/0000.sql b/tests/dirstructure2/migrations/down/0000.sql new file mode 100644 index 0000000..e69de29 diff --git a/tests/dirstructure2/migrations/down/00001.sql b/tests/dirstructure2/migrations/down/00001.sql new file mode 100644 index 0000000..e69de29 diff --git a/tests/dirstructure2/migrations/down/00002.sql b/tests/dirstructure2/migrations/down/00002.sql new file mode 100644 index 0000000..e69de29 diff --git a/tests/dirstructure2/migrations/up/00001.sql b/tests/dirstructure2/migrations/up/00001.sql new file mode 100644 index 0000000..e69de29 diff --git a/tests/dirstructure2/migrations/up/00002.sql b/tests/dirstructure2/migrations/up/00002.sql new file mode 100644 index 0000000..760589c --- /dev/null +++ b/tests/dirstructure2/migrations/up/00002.sql @@ -0,0 +1 @@ +error \ No newline at end of file diff --git a/tests/dirstructure2/migrations/up/00003.sql b/tests/dirstructure2/migrations/up/00003.sql new file mode 100644 index 0000000..e69de29 From 20c14940d6289a91cc265fa9f9f4d603a85be809 Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Tue, 18 Jul 2023 23:38:51 -0500 Subject: [PATCH 3/5] Address issue #49 - Doesn't work for MySQL --- README.md | 7 ++++++- src/Database/AbstractDatabase.php | 5 +++++ src/Database/DatabaseInterface.php | 2 ++ src/Database/MySqlDatabase.php | 7 +++++++ src/Migration.php | 8 +++++--- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 74c986b..2be61fa 100644 --- a/README.md +++ b/README.md @@ -205,7 +205,7 @@ $migration->getDbDriver(); To use it, please visit: -### Avoiding Partial Migration +### Avoiding Partial Migration (not available for MySQL) A partial migration is when the migration script is interrupted in the middle of the process due to an error or a manual interruption. @@ -221,6 +221,11 @@ To enable this feature you need to call the method `withTransactionEnabled` pass withTransactionEnabled(true); ``` + +**NOTE: This feature isn't available for MySQL as it doesn't support DDL commands inside a transaction.** +If you use this method with MySQL the Migration will ignore it silently. +More info: [https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html](https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html) + ## Tips on writing SQL migrations for Postgres ### On creating triggers and SQL functions diff --git a/src/Database/AbstractDatabase.php b/src/Database/AbstractDatabase.php index b46c172..a4cd160 100644 --- a/src/Database/AbstractDatabase.php +++ b/src/Database/AbstractDatabase.php @@ -141,4 +141,9 @@ public function isDatabaseVersioned() { return $this->isTableExists(ltrim($this->getDbDriver()->getUri()->getPath(), "/"), $this->getMigrationTable()); } + + public function supportsTransaction() + { + return true; + } } diff --git a/src/Database/DatabaseInterface.php b/src/Database/DatabaseInterface.php index ddb9696..1fb1826 100644 --- a/src/Database/DatabaseInterface.php +++ b/src/Database/DatabaseInterface.php @@ -34,4 +34,6 @@ public function isDatabaseVersioned(); public function getDbDriver(); public function getMigrationTable(); + + public function supportsTransaction(); } diff --git a/src/Database/MySqlDatabase.php b/src/Database/MySqlDatabase.php index 63f068d..d8238d3 100644 --- a/src/Database/MySqlDatabase.php +++ b/src/Database/MySqlDatabase.php @@ -52,4 +52,11 @@ public function executeSql($sql) { $this->getDbDriver()->execute($sql); } + + public function supportsTransaction() + { + // MySQL doesn't support transaction for DDL commands + // https://dev.mysql.com/doc/refman/8.0/en/cannot-roll-back.html + return false; + } } diff --git a/src/Migration.php b/src/Migration.php index b19a242..4a8fb80 100644 --- a/src/Migration.php +++ b/src/Migration.php @@ -333,18 +333,20 @@ protected function migrate($upVersion, $increment, $force) call_user_func_array($this->callableProgress, ['migrate', $currentVersion, $fileInfo]); } + $useTransaction = $this->transaction && $this->getDbCommand()->supportsTransaction(); + try { - if ($this->transaction) { + if ($useTransaction) { $this->getDbDriver()->beginTransaction(); } $this->getDbCommand()->setVersion($currentVersion, Migration::VERSION_STATUS_PARTIAL . ' ' . ($increment>0 ? 'up' : 'down')); $this->getDbCommand()->executeSql($fileInfo["content"]); $this->getDbCommand()->setVersion($currentVersion, Migration::VERSION_STATUS_COMPLETE); - if ($this->transaction) { + if ($useTransaction) { $this->getDbDriver()->commitTransaction(); } } catch (\Exception $e) { - if ($this->transaction) { + if ($useTransaction) { $this->getDbDriver()->rollbackTransaction(); } throw $e; From 29f74e52a9b2db7dc18bd437ac67a6599b1b2e1c Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Sat, 30 Dec 2023 15:01:22 -0600 Subject: [PATCH 4/5] Add dependencies --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 2be61fa..03147fc 100644 --- a/README.md +++ b/README.md @@ -391,5 +391,14 @@ export SQLITE_TEST_HOST=/tmp/test.db # defaults to /tmp/test.db * [PHP Rest Template](https://github.com/byjg/php-rest-template) * [USDocker](https://github.com/usdocker/usdocker) +## Dependencies + +```mermaid +flowchart TD + byjg/migration --> byjg/anydataset-db + byjg/migration --> ext-pdo +``` + + ---- [Open source ByJG](http://opensource.byjg.com) From d7ba88ded15c1b416fee354f199cc2f1632af6fe Mon Sep 17 00:00:00 2001 From: Joao Gilberto Magalhaes Date: Fri, 5 Jan 2024 14:20:25 -0600 Subject: [PATCH 5/5] Add dependencies --- .github/workflows/phpunit.yml | 22 +++++++++------------- README.md | 8 ++++---- composer.json | 2 +- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 5d163c0..daa243b 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -18,14 +18,10 @@ jobs: strategy: matrix: php-version: + - "8.2" - "8.1" - "8.0" - "7.4" - - "7.3" -# - "7.2" -# - "7.1" -# - "7.0" -# - "5.6" services: mysql: @@ -69,7 +65,7 @@ jobs: MSSQL_TEST_HOST: sqlserver steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # - name: Spin up databases # run: | # apk add --no-cache python3 python3-dev py3-pip build-base libffi-dev @@ -86,11 +82,11 @@ jobs: # - run: ./vendor/bin/phpunit tests/SqlServerDblib* Documentation: - runs-on: 'ubuntu-latest' - needs: Build if: github.ref == 'refs/heads/master' - env: - DOC_GITHUB_TOKEN: '${{ secrets.DOC_TOKEN }}' - steps: - - uses: actions/checkout@v3 - - run: curl https://opensource.byjg.com/add-doc.sh | bash /dev/stdin php migration + needs: Build + uses: byjg/byjg.github.io/.github/workflows/add-doc.yaml@master + with: + folder: php + project: ${{ github.event.repository.name }} + secrets: inherit + diff --git a/README.md b/README.md index 03147fc..15627f9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Database Migrations PHP [![Opensource ByJG](https://img.shields.io/badge/opensource-byjg-success.svg)](http://opensource.byjg.com) -[![GitHub source](https://img.shields.io/badge/Github-source-informational?logo=github)](https://github.com/byjg/migration/) -[![GitHub license](https://img.shields.io/github/license/byjg/migration.svg)](https://opensource.byjg.com/opensource/licensing.html) -[![GitHub release](https://img.shields.io/github/release/byjg/migration.svg)](https://github.com/byjg/migration/releases/) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/byjg/migration/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/byjg/migration/?branch=master) +[![GitHub source](https://img.shields.io/badge/Github-source-informational?logo=github)](https://github.com/byjg/php-migration/) +[![GitHub license](https://img.shields.io/github/license/byjg/php-migration.svg)](https://opensource.byjg.com/opensource/licensing.html) +[![GitHub release](https://img.shields.io/github/release/byjg/php-migration.svg)](https://github.com/byjg/php-migration/releases/) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/byjg/php-migration/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/byjg/php-migration/?branch=master) [![Build Status](https://github.com/byjg/migration/actions/workflows/phpunit.yml/badge.svg?branch=master)](https://github.com/byjg/migration/actions/workflows/phpunit.yml) ## Features diff --git a/composer.json b/composer.json index 393ed55..30620fb 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,7 @@ "ext-pdo": "*" }, "require-dev": { - "phpunit/phpunit": "5.7.*|7.4.*|^9.5" + "phpunit/phpunit": "5.7.*|7.4.*|^9.6" }, "autoload": { "psr-4": {