diff --git a/.idea/runConfigurations/Run_Databases.xml b/.idea/runConfigurations/Run_Databases.xml new file mode 100644 index 0000000..56bd727 --- /dev/null +++ b/.idea/runConfigurations/Run_Databases.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Start_MySql.xml b/.idea/runConfigurations/Start_MySql.xml deleted file mode 100644 index 6ec0abf..0000000 --- a/.idea/runConfigurations/Start_MySql.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Start_Postgres.xml b/.idea/runConfigurations/Start_Postgres.xml deleted file mode 100644 index 95c3e67..0000000 --- a/.idea/runConfigurations/Start_Postgres.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Start_SQL_Server.xml b/.idea/runConfigurations/Start_SQL_Server.xml deleted file mode 100644 index 81de896..0000000 --- a/.idea/runConfigurations/Start_SQL_Server.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Test_SqlSever.xml b/.idea/runConfigurations/Test_SqlSever.xml deleted file mode 100644 index cd28e03..0000000 --- a/.idea/runConfigurations/Test_SqlSever.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations/Test_SqlSever_Dblib.xml b/.idea/runConfigurations/Test_SqlSever_Dblib.xml new file mode 100644 index 0000000..92d3c9a --- /dev/null +++ b/.idea/runConfigurations/Test_SqlSever_Dblib.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/Test_SqlSever_Sqlsrv.xml b/.idea/runConfigurations/Test_SqlSever_Sqlsrv.xml new file mode 100644 index 0000000..e8d0b84 --- /dev/null +++ b/.idea/runConfigurations/Test_SqlSever_Sqlsrv.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 533f665..7ffb61c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,5 @@ language: php -addons: - hosts: - - mysql-container - - postgres-container - - dblib-container - php: - "7.3" - "7.2" @@ -19,21 +13,22 @@ services: before_install: - sudo service mysql stop || echo "mysql not stopped" - sudo service postgresql stop || echo "postgresql not stopped" -# - docker run --name mssql-container --rm -e ACCEPT_EULA=Y -e SA_PASSWORD=Pa55word -p 1433:1433 -d mcr.microsoft.com/mssql/server - - docker run --name postgres-container --rm -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres:9-alpine - - docker run --name mysql-container --rm -e MYSQL_ROOT_PASSWORD=password -p 3306:3306 -d mysql:5.7 + - docker-compose up -d postgres mysql install: - composer install script: - vendor/bin/phpunit - - vendor/bin/phpunit tests/PostgresDatabaseTest.php - - vendor/bin/phpunit tests/MysqlDatabaseTest.php - - vendor/bin/phpunit tests/SqlServerDatabaseTest.php - - - vendor/bin/phpunit tests/SqliteDatabaseCustomTest.php - - vendor/bin/phpunit tests/PostgresDatabaseCustomTest.php - - vendor/bin/phpunit tests/MysqlDatabaseCustomTest.php - - vendor/bin/phpunit tests/SqlServerDatabaseCustomTest.php + - vendor/bin/phpunit tests/SqliteDatabase* + - ./wait-for-db.sh postgres && vendor/bin/phpunit tests/PostgresDatabase* + - ./wait-for-db.sh mysql && vendor/bin/phpunit tests/MysqlDatabase* +# - ./wait-for-db.sh mssql && vendor/bin/phpunit tests/SqlServer* + +jobs: + include: + - stage: documentation + if: branch = master + install: skip + script: "curl https://opensource.byjg.com/add-doc.sh | bash /dev/stdin php migration" diff --git a/README.md b/README.md index 8628501..c9e331a 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,14 @@ # Database Migrations PHP -[![Opensource ByJG](https://img.shields.io/badge/opensource-byjg.com-brightgreen.svg)](http://opensource.byjg.com) +[![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) [![SensioLabsInsight](https://insight.sensiolabs.com/projects/571cb412-7018-4938-a4e5-0f9ce44956d7/mini.png)](https://insight.sensiolabs.com/projects/571cb412-7018-4938-a4e5-0f9ce44956d7) -[![Build Status](https://travis-ci.org/byjg/migration.svg?branch=master)](https://travis-ci.org/byjg/migration) +[![Build Status](https://travis-ci.com/byjg/migration.svg?branch=master)](https://travis-ci.com/byjg/migration) + +## Features This is a simple library written in PHP for database version control. Currently supports Sqlite, MySql, Sql Server and Postgres. @@ -14,9 +19,9 @@ Database Migration can be used as: Database Migrates uses only SQL commands for versioning your database. -**Why pure SQL commands?** +## Why pure SQL commands? -The most of frameworks tend to use programming statements for versioning your database instead of use pure SQL. +The most of the frameworks tend to use programming statements for versioning your database instead of use pure SQL. There are some advantages to use the native programming language of your framework to maintain the database: - Framework commands have some trick codes to do complex tasks; @@ -27,35 +32,39 @@ But at the end despite these good features the reality in big projects someone w Because of that this is an agnostic project (independent of framework and Programming Language) and use pure and native SQL commands for migrate your database. -# Installing +## Installing -## PHP Library +### PHP Library If you want to use only the PHP Library in your project: ``` -composer require "byjg/migration":"4.0.*" +composer require "byjg/migration":"4.2.*" ``` -## Command Line Interface +### Command Line Interface The command line interface is standalone and does not require you install with your project. You can install global and create a symbolic lynk ``` -composer require "byjg/migration-cli":"4.0.*" +composer require "byjg/migration-cli":"4.1.*" ``` Please visit https://github.com/byjg/migration-cli to get more informations about Migration CLI. -# Supported databases: +## Supported databases: + +| Database | Driver | Connection String | +| --------------| ------------------------------------------------------------------------------- | -------------------------------------------------------- | +| Sqlite | [pdo_sqlite](https://www.php.net/manual/en/ref.pdo-sqlite.php) | sqlite:///path/to/file | +| MySql/MariaDb | [pdo_mysql](https://www.php.net/manual/en/ref.pdo-mysql.php) | mysql://username:password@hostname:port/database | +| Postgres | [pdo_pgsql](https://www.php.net/manual/en/ref.pdo-pgsql.php) | psql://username:password@hostname:port/database | +| Sql Server | [pdo_dblib, pdo_sysbase](https://www.php.net/manual/en/ref.pdo-dblib.php) Linux | dblib://username:password@hostname:port/database | +| Sql Server | [pdo_sqlsrv](http://msdn.microsoft.com/en-us/sqlserver/ff657782.aspx) Windows | sqlsrv://username:password@hostname:port/database | - * Sqlite - * Mysql / MariaDB - * Postgres - * SqlServer -# How It Works? +## How It Works? The Database Migration uses PURE SQL to manage the database versioning. In order to get working you need to: @@ -63,7 +72,7 @@ In order to get working you need to: - Create the SQL Scripts - Manage using Command Line or the API. -## The SQL Scripts +### The SQL Scripts The scripts are divided in three set of scripts: @@ -97,7 +106,7 @@ The directory scripts is : For example: 00001.sql is the script for move the database from version '2' to '1'. The "down" folder is optional. -**Multi Development environment** +### Multi Development environment If you work with multiple developers and multiple branches it is to difficult to determine what is the next number. @@ -118,7 +127,7 @@ the migration script will down and alert him there a TWO versions 43. In that ca file do 44-dev.sql and continue to work until merge your changes and generate a final version. -# Using the PHP API and Integrate it into your projects. +## Using the PHP API and Integrate it into your projects. The basic usage is @@ -141,6 +150,11 @@ $migration = new \ByJG\DbMigration\Migration($connectionUri, '.'); $migration->registerDatabase('mysql', \ByJG\DbMigration\Database\MySqlDatabase::class); $migration->registerDatabase('maria', \ByJG\DbMigration\Database\MySqlDatabase::class); +// Add a callback progress function to receive info from the execution +$migration->addCallbackProgress(function ($action, $currentVersion, $fileInfo) { + echo "$action, $currentVersion, ${fileInfo['description']}\n"; +}); + // Restore the database using the "base.sql" script // and run ALL existing scripts for up the database version to the latest version $migration->reset(); @@ -154,7 +168,7 @@ $migration->update($version = null); The Migration object controls the database version. -## Creating a version control in your project: +### Creating a version control in your project: ```php registerDatabase('mysql', \ByJG\DbMigration\Database\MySqlDatabase:: $migration->createVersion(); ``` -## Getting the current version +### Getting the current version ```php getCurrentVersion(); ``` -## Add Callback to control the progress +### Add Callback to control the progress ```php addCallbackProgress(function ($command, $version) { - echo "Doing Command: $command at version $version"; +$migration->addCallbackProgress(function ($command, $version, $fileInfo) { + echo "Doing Command: $command at version $version - ${fileInfo['description']}, ${fileInfo['exists']}, ${fileInfo['file']}, ${fileInfo['checksum']}\n"; }); ``` -## Getting the Db Driver instance +### Getting the Db Driver instance ```php getDbDriver(); To use it, please visit: https://github.com/byjg/anydataset-db -# Tips on writing SQL migrations +## Tips on writing SQL migrations for Postgres -## Rely on explicit transactions +### Rely on explicit transactions ```sql -- DO @@ -228,7 +242,7 @@ and warn you when you attempt to run it again. The difference is that with expli transactions you know that the database cannot be in an inconsistent state after an unexpected failure. -## On creating triggers and SQL functions +### On creating triggers and SQL functions ```sql -- DO @@ -291,7 +305,7 @@ comment after every inner semicolon of a function definition `byjg/migration` wi Unfortunately, if you forget to add any of these comments the library will split the `CREATE FUNCTION` statement in multiple parts and the migration will fail. -## Avoid the colon character (`:`) +### Avoid the colon character (`:`) ```sql -- DO @@ -319,17 +333,17 @@ read this as an invalid named parameter in an invalid context and fail when it t The only way to fix this inconsistency is avoiding colons altogether (in this case, PostgreSQL also has an alternative syntax: `CAST(value AS type)`). -## Use an SQL editor +### Use an SQL editor Finally, writing manual SQL migrations can be tiresome, but it is significantly easier if you use an editor capable of understanding the SQL syntax, providing autocomplete, introspecting your current database schema and/or autoformatting your code. -# Handle different migration inside one schema +## Handling different migration inside one schema If you need to create different migration scripts and version inside the same schema it is possible -but is too risky and I do not recommend at all. +but is too risky and I **do not** recommend at all. To do this, you need to create different "migration tables" by passing the parameter to the constructor. @@ -344,63 +358,55 @@ For security reasons, this feature is not available at command line, but you can We really recommend do not use this feature. The recommendation is one migration for one schema. -# Unit Tests - -This library has integrated tests and need to be setup for each database you want to test. +## Running Unit tests -Basiclly you have the follow tests: +Basic unit tests can be running by: +```bash +vendor/bin/phpunit ``` -vendor/bin/phpunit tests/SqliteDatabaseTest.php -vendor/bin/phpunit tests/MysqlDatabaseTest.php -vendor/bin/phpunit tests/PostgresDatabaseTest.php -vendor/bin/phpunit tests/SqlServerDatabaseTest.php -``` -## Using Docker for testing +## Running database tests + +Run integration tests require you to have the databases up and running. We provided a basic `docker-compose.yml` and you +can use to start the databases for test. -### MySql +**Running the databases** ```bash -docker run --name mysql-container --rm -e MYSQL_ROOT_PASSWORD=password -p 3306:3306 -d mysql:5.7 - -docker run -it --rm \ - --link mysql-container \ - -v $PWD:/work \ - -w /work \ - byjg/php:7.2-cli \ - phpunit tests/MysqlDatabaseTest +docker-compose up -d postgres mysql mssql ``` -### Postgresql +**Run the tests** -```bash -docker run --name postgres-container --rm -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password -p 5432:5432 -d postgres:9-alpine - -docker run -it --rm \ - --link postgres-container \ - -v $PWD:/work \ - -w /work \ - byjg/php:7.2-cli \ - phpunit tests/PostgresDatabaseTest +``` +vendor/bin/phpunit +vendor/bin/phpunit tests/SqliteDatabase* +vendor/bin/phpunit tests/MysqlDatabase* +vendor/bin/phpunit tests/PostgresDatabase* +vendor/bin/phpunit tests/SqlServerDblibDatabase* +vendor/bin/phpunit tests/SqlServerSqlsrvDatabase* ``` -### Microsoft SqlServer +Optionally you can set the host and password used by the unit tests ```bash -docker run --name mssql-container --rm -e ACCEPT_EULA=Y -e SA_PASSWORD=Pa55word -p 1433:1433 -d mcr.microsoft.com/mssql/server - -docker run -it --rm \ - --link mssql-container \ - -v $PWD:/work \ - -w /work \ - byjg/php:7.2-cli \ - phpunit tests/SqlServerDatabaseTest +export MYSQL_TEST_HOST=localhost # defaults to localhost +export MYSQL_PASSWORD=newpassword # use '.' if want have a null password +export PSQL_TEST_HOST=localhost # defaults to localhost +export PSQL_PASSWORD=newpassword # use '.' if want have a null password +export MSSQL_TEST_HOST=localhost # defaults to localhost +export MSSQL_PASSWORD=Pa55word +export SQLITE_TEST_HOST=/tmp/test.db # defaults to /tmp/test.db ``` -# Related Projects + +## Related Projects - [Micro ORM](https://github.com/byjg/micro-orm) - [Anydataset](https://github.com/byjg/anydataset) - [PHP Rest Template](https://github.com/byjg/php-rest-template) - [USDocker](https://github.com/usdocker/usdocker) + +---- +[Open source ByJG](http://opensource.byjg.com) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3ef479a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +version: '3.4' +services: + mssql: + container_name: anydataset_db_mssql + image: mcr.microsoft.com/mssql/server + environment: + - ACCEPT_EULA=Y + - SA_PASSWORD=Pa55word + ports: + - "1433:1433" + healthcheck: + test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-S", "localhost,1433", "-U", "sa", "-P", "Pa55word", "-Q", "SELECT 1"] + timeout: 20s + interval: 10s + retries: 10 + + postgres: + container_name: anydataset_db_postgres + image: postgres:9-alpine + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=password + ports: + - "5432:5432" + healthcheck: + test: [ "CMD", "pg_isready", "-q", "-h", "localhost", "-U", "postgres" ] + timeout: 20s + interval: 10s + retries: 10 + + mysql: + container_name: anydataset_db_mysql + image: mysql:5.7 + environment: + - MYSQL_ROOT_PASSWORD=password + ports: + - "3306:3306" + healthcheck: + test: [ "CMD", "mysqladmin" ,"ping", "-h", "localhost" ] + timeout: 20s + interval: 10s + retries: 10 diff --git a/example/mysql/migrations/down/00001.sql b/example/mysql/migrations/down/00001.sql index 2d5d1aa..89ee02c 100644 --- a/example/mysql/migrations/down/00001.sql +++ b/example/mysql/migrations/down/00001.sql @@ -6,4 +6,4 @@ -- This is the reverse operation of the script up/00002 -- -------------------------------------------------------- -drop table roles; +drop table posts; diff --git a/example/mysql/migrations/up/00002.sql b/example/mysql/migrations/up/00002.sql index fee9d47..e998144 100644 --- a/example/mysql/migrations/up/00002.sql +++ b/example/mysql/migrations/up/00002.sql @@ -4,9 +4,13 @@ -- from version '1' to version '2' -- -------------------------------------------------------- -create table roles +create table posts ( id int NOT NULL AUTO_INCREMENT PRIMARY KEY, - rolename char(1) NOT NULL, - userid int not null -) + userid int not null, + title varchar(50) not null, + post text not null, + CONSTRAINT fk_posts_users foreign key (userid) references users(id) +); + +insert into posts (userid, title, post) values (1, 'Testing', '\\n

This is an example page. It\'s different from a blog post because it will stay in one place and will show up in your site navigation (in most themes). Most people start with an About page that introduces them to potential site visitors. It might say something like this:

\\n\\n\\n\\n

Hi there! I\'m a bike messenger by day, aspiring actor by night, and this is my website. I live in Los Angeles, have a great dog named Jack, and I like piña coladas. (And gettin\' caught in the rain.)

\\n\\n\\n\\n

...or something like this:

\\n\\n\\n\\n

The XYZ Doohickey Company was founded in 1971, and has been providing quality doohickeys to the public ever since. Located in Gotham City, XYZ employs over 2,000 people and does all kinds of awesome things for the Gotham community.

\\n\\n\\n\\n

As a new WordPress user, you should go to your dashboard to delete this page and create new pages for your content. Have fun!

\\n'); diff --git a/example/postgres/migrations/down/00001.sql b/example/postgres/migrations/down/00001.sql index 2d5d1aa..89ee02c 100644 --- a/example/postgres/migrations/down/00001.sql +++ b/example/postgres/migrations/down/00001.sql @@ -6,4 +6,4 @@ -- This is the reverse operation of the script up/00002 -- -------------------------------------------------------- -drop table roles; +drop table posts; diff --git a/example/postgres/migrations/up/00002.sql b/example/postgres/migrations/up/00002.sql index 8235099..1c70d6e 100644 --- a/example/postgres/migrations/up/00002.sql +++ b/example/postgres/migrations/up/00002.sql @@ -4,9 +4,13 @@ -- from version '1' to version '2' -- -------------------------------------------------------- -create table roles +create table posts ( id SERIAL NOT NULL PRIMARY KEY, - rolename char(1) NOT NULL, - userid int not null -) + userid int not null, + title varchar(50) not null, + post text not null, + CONSTRAINT fk_posts_users foreign key (userid) references users(id) +); + +insert into posts (userid, title, post) values (1, 'Testing', '\n

This is an example page. It''s different from a blog post because it will stay in one place and will show up in your site navigation (in most themes). Most people start with an About page that introduces them to potential site visitors. It might say something like this:

\n\n\n\n

Hi there! I''m a bike messenger by day, aspiring actor by night, and this is my website. I live in Los Angeles, have a great dog named Jack, and I like piña coladas. (And gettin'' caught in the rain.)

\n\n\n\n

...or something like this:

\n\n\n\n

The XYZ Doohickey Company was founded in 1971, and has been providing quality doohickeys to the public ever since. Located in Gotham City, XYZ employs over 2,000 people and does all kinds of awesome things for the Gotham community.

\n\n\n\n

As a new WordPress user, you should go to your dashboard to delete this page and create new pages for your content. Have fun!

\n'); diff --git a/example/sql_server/migrations/down/00001.sql b/example/sql_server/migrations/down/00001.sql index 2d5d1aa..89ee02c 100644 --- a/example/sql_server/migrations/down/00001.sql +++ b/example/sql_server/migrations/down/00001.sql @@ -6,4 +6,4 @@ -- This is the reverse operation of the script up/00002 -- -------------------------------------------------------- -drop table roles; +drop table posts; diff --git a/example/sql_server/migrations/up/00002.sql b/example/sql_server/migrations/up/00002.sql index 05eefe5..303d8eb 100644 --- a/example/sql_server/migrations/up/00002.sql +++ b/example/sql_server/migrations/up/00002.sql @@ -4,9 +4,13 @@ -- from version '1' to version '2' -- -------------------------------------------------------- -create table roles +create table posts ( ID int IDENTITY(1,1) PRIMARY KEY, - rolename char(1) NOT NULL, - userid int not null -) + userid int not null, + title varchar(50) not null, + post text not null, + CONSTRAINT fk_posts_users foreign key (userid) references users(id) +); + +insert into posts (userid, title, post) values (1, 'Testing', '\n

This is an example page. It''s different from a blog post because it will stay in one place and will show up in your site navigation (in most themes). Most people start with an About page that introduces them to potential site visitors. It might say something like this:

\n\n\n\n

Hi there! I''m a bike messenger by day, aspiring actor by night, and this is my website. I live in Los Angeles, have a great dog named Jack, and I like piña coladas. (And gettin'' caught in the rain.)

\n\n\n\n

...or something like this:

\n\n\n\n

The XYZ Doohickey Company was founded in 1971, and has been providing quality doohickeys to the public ever since. Located in Gotham City, XYZ employs over 2,000 people and does all kinds of awesome things for the Gotham community.

\n\n\n\n

As a new WordPress user, you should go to your dashboard to delete this page and create new pages for your content. Have fun!

\n'); diff --git a/example/sqlite/migrations/down/00001.sql b/example/sqlite/migrations/down/00001.sql index 2d5d1aa..89ee02c 100644 --- a/example/sqlite/migrations/down/00001.sql +++ b/example/sqlite/migrations/down/00001.sql @@ -6,4 +6,4 @@ -- This is the reverse operation of the script up/00002 -- -------------------------------------------------------- -drop table roles; +drop table posts; diff --git a/example/sqlite/migrations/up/00002.sql b/example/sqlite/migrations/up/00002.sql index d573f83..2cab175 100644 --- a/example/sqlite/migrations/up/00002.sql +++ b/example/sqlite/migrations/up/00002.sql @@ -4,9 +4,13 @@ -- from version '1' to version '2' -- -------------------------------------------------------- -create table roles +create table posts ( id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - rolename char(1) NOT NULL, - userid int not null -) + userid int not null, + title varchar(50) not null, + post text not null, + CONSTRAINT fk_posts_users foreign key (userid) references users(id) +); + +insert into posts (userid, title, post) values (1, 'Testing', '\n

This is an example page. It''s different from a blog post because it will stay in one place and will show up in your site navigation (in most themes). Most people start with an About page that introduces them to potential site visitors. It might say something like this:

\n\n\n\n

Hi there! I''m a bike messenger by day, aspiring actor by night, and this is my website. I live in Los Angeles, have a great dog named Jack, and I like piña coladas. (And gettin'' caught in the rain.)

\n\n\n\n

...or something like this:

\n\n\n\n

The XYZ Doohickey Company was founded in 1971, and has been providing quality doohickeys to the public ever since. Located in Gotham City, XYZ employs over 2,000 people and does all kinds of awesome things for the Gotham community.

\n\n\n\n

As a new WordPress user, you should go to your dashboard to delete this page and create new pages for your content. Have fun!

\n'); diff --git a/src/Database/DblibDatabase.php b/src/Database/DblibDatabase.php index bc8ad2c..ff03b7c 100644 --- a/src/Database/DblibDatabase.php +++ b/src/Database/DblibDatabase.php @@ -67,6 +67,9 @@ public function executeSql($sql) $statements = preg_split("/;(\r\n|\r|\n)/", $sql); foreach ($statements as $sql) { + if (empty(trim($sql))) { + continue; + } $this->executeSqlInternal($sql); } } @@ -75,4 +78,24 @@ protected function executeSqlInternal($sql) { $this->getDbDriver()->execute($sql); } + + /** + * @param $schema + * @param $table + * @return bool + */ + protected function isTableExists($schema, $table) + { + $count = $this->getDbDriver()->getScalar( + 'SELECT count(*) FROM information_schema.tables ' . + ' WHERE table_catalog = [[schema]] ' . + ' AND table_name = [[table]] ', + [ + "schema" => $schema, + "table" => $table + ] + ); + + return (intval($count) !== 0); + } } diff --git a/src/Migration.php b/src/Migration.php index fe2be0b..fcc8e41 100644 --- a/src/Migration.php +++ b/src/Migration.php @@ -170,6 +170,36 @@ public function getMigrationSql($version, $increment) return null; } + /** + * Get the file contents and metainfo + * @param $file + * @return array + */ + public function getFileContent($file) + { + $data = [ + "file" => $file, + "description" => "no description provided. Pro tip: use `-- @description:` to define one.", + "exists" => false, + "checksum" => null, + "content" => null, + ]; + if (!file_exists($file)) { + return $data; + } + + $data["content"] = file_get_contents($file); + + if (preg_match("/--\s*@description:\s*(?.*)/", $data["content"], $description)) { + $data["description"] = $description["name"]; + } + + $data["exists"] = true; + $data["checksum"] = sha1($data["content"]); + + return $data; + } + /** * Create the database it it does not exists. Does not use this methos in a production environment * @@ -194,15 +224,17 @@ public function prepareEnvironment() */ public function reset($upVersion = null) { + $fileInfo = $this->getFileContent($this->getBaseSql()); + if ($this->callableProgress) { - call_user_func_array($this->callableProgress, ['reset', 0]); + call_user_func_array($this->callableProgress, ['reset', 0, $fileInfo]); } $this->getDbCommand()->dropDatabase(); $this->getDbCommand()->createDatabase(); $this->getDbCommand()->createVersion(); - if (file_exists($this->getBaseSql())) { - $this->getDbCommand()->executeSql(file_get_contents($this->getBaseSql())); + if ($fileInfo["exists"]) { + $this->getDbCommand()->executeSql($fileInfo["content"]); } $this->getDbCommand()->setVersion(0, Migration::VERSION_STATUS_COMPLETE); @@ -277,14 +309,19 @@ protected function migrate($upVersion, $increment, $force) } while ($this->canContinue($currentVersion, $upVersion, $increment) - && file_exists($file = $this->getMigrationSql($currentVersion, $increment)) ) { + $fileInfo = $this->getFileContent($this->getMigrationSql($currentVersion, $increment)); + + if (!$fileInfo["exists"]) { + break; + } + if ($this->callableProgress) { - call_user_func_array($this->callableProgress, ['migrate', $currentVersion]); + call_user_func_array($this->callableProgress, ['migrate', $currentVersion, $fileInfo]); } $this->getDbCommand()->setVersion($currentVersion, Migration::VERSION_STATUS_PARTIAL . ' ' . ($increment>0 ? 'up' : 'down')); - $this->getDbCommand()->executeSql(file_get_contents($file)); + $this->getDbCommand()->executeSql($fileInfo["content"]); $this->getDbCommand()->setVersion($currentVersion, Migration::VERSION_STATUS_COMPLETE); $currentVersion = $currentVersion + $increment; } diff --git a/tests/BaseDatabase.php b/tests/BaseDatabase.php index 4a1cf3f..4280f89 100644 --- a/tests/BaseDatabase.php +++ b/tests/BaseDatabase.php @@ -132,6 +132,13 @@ protected function getExpectedUsersVersion1() ]; } + protected function getExpectedPostsVersion2() + { + return [ + ["id" => 1, "userid" => 1, "title" => 'Testing', 'post' => "\\n

This is an example page. It's different from a blog post because it will stay in one place and will show up in your site navigation (in most themes). Most people start with an About page that introduces them to potential site visitors. It might say something like this:

\\n\\n\\n\\n

Hi there! I'm a bike messenger by day, aspiring actor by night, and this is my website. I live in Los Angeles, have a great dog named Jack, and I like piña coladas. (And gettin' caught in the rain.)

\\n\\n\\n\\n

...or something like this:

\\n\\n\\n\\n

The XYZ Doohickey Company was founded in 1971, and has been providing quality doohickeys to the public ever since. Located in Gotham City, XYZ employs over 2,000 people and does all kinds of awesome things for the Gotham community.

\\n\\n\\n\\n

As a new WordPress user, you should go to your dashboard to delete this page and create new pages for your content. Have fun!

\\n"], + ]; + } + /** * @throws \ByJG\DbMigration\Exception\DatabaseDoesNotRegistered * @throws \ByJG\Serializer\Exception\InvalidArgumentException @@ -215,6 +222,7 @@ protected function assertVersion2() $status = $this->migrate->getDbDriver()->getScalar('select status from '. $this->migrationTable); $this->assertEquals(Migration::VERSION_STATUS_COMPLETE, $status); + // Users $iterator = $this->migrate->getDbDriver()->getIterator('select * from users'); $this->assertTrue($iterator->hasNext()); @@ -233,7 +241,15 @@ protected function assertVersion2() $this->assertFalse($iterator->hasNext()); - $this->migrate->getDbDriver()->getIterator('select * from roles'); + // Posts + $iterator = $this->migrate->getDbDriver()->getIterator('select * from posts'); + + $this->assertTrue($iterator->hasNext()); + $row = $iterator->moveNext(); + $this->assertEquals( + $this->getExpectedPostsVersion2()[0], + $row->toArray() + ); } /** diff --git a/tests/MigrationTest.php b/tests/MigrationTest.php index 0f7351f..8a2135c 100644 --- a/tests/MigrationTest.php +++ b/tests/MigrationTest.php @@ -96,4 +96,60 @@ public function testGetMigrationSqlDown4() { $this->object->getMigrationSql(13, -1); } + + public function testGetFileContent_NonExists() + { + $this->assertEquals( + [ + "file" => "non-existent", + "description" => "no description provided. Pro tip: use `-- @description:` to define one.", + "exists" => false, + "checksum" => null, + "content" => null, + ], + $this->object->getFileContent("non-existent") + ); + } + + public function testGetFileContent_1() + { + $this->assertEquals( + [ + "file" => __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", + ], + $this->object->getFileContent(__DIR__ . '/dirstructure/migrations/up/00001.sql') + ); + } + + public function testGetFileContent_2() + { + $this->assertEquals( + [ + "file" => __DIR__ . '/dirstructure/migrations/up/00002.sql', + "description" => "another test", + "exists" => true, + "checksum" => "fd8ab8176291c2dcbf0d91564405e0f98f0cd77e", + "content" => "-- @description: another test\n\nselect * from dual;", + ], + $this->object->getFileContent(__DIR__ . '/dirstructure/migrations/up/00002.sql') + ); + } + + public function testGetFileContent_3() + { + $this->assertEquals( + [ + "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;", + ], + $this->object->getFileContent(__DIR__ . '/dirstructure/migrations/up/00003.sql') + ); + } } diff --git a/tests/MysqlDatabaseTest.php b/tests/MysqlDatabaseTest.php index b341d46..79e746c 100644 --- a/tests/MysqlDatabaseTest.php +++ b/tests/MysqlDatabaseTest.php @@ -1,5 +1,9 @@ migrate = new \ByJG\DbMigration\Migration(new \ByJG\Util\Uri($this->uri), __DIR__ . '/../example/mysql', true, $this->migrationTable); - $this->migrate->registerDatabase("mysql", \ByJG\DbMigration\Database\MySqlDatabase::class); + $host = getenv('MYSQL_TEST_HOST'); + if (empty($host)) { + $host = "127.0.0.1"; + } + $password = getenv('MYSQL_PASSWORD'); + if (empty($password)) { + $password = 'password'; + } + if ($password == '.') { + $password = ""; + } + + $uri = "mysql://root:${password}@${host}/migratedatabase"; + + $this->migrate = new Migration(new Uri($uri), __DIR__ . '/../example/mysql', true, $this->migrationTable); + $this->migrate->registerDatabase("mysql", MySqlDatabase::class); parent::setUp(); } } diff --git a/tests/PostgresDatabaseTest.php b/tests/PostgresDatabaseTest.php index 96a8ff5..bc36f98 100644 --- a/tests/PostgresDatabaseTest.php +++ b/tests/PostgresDatabaseTest.php @@ -1,5 +1,9 @@ migrate = new \ByJG\DbMigration\Migration(new \ByJG\Util\Uri($this->uri), __DIR__ . '/../example/postgres', true, $this->migrationTable); - $this->migrate->registerDatabase("pgsql", \ByJG\DbMigration\Database\PgsqlDatabase::class); + $host = getenv('PSQL_TEST_HOST'); + if (empty($host)) { + $host = "127.0.0.1"; + } + $password = getenv('PSQL_PASSWORD'); + if (empty($password)) { + $password = 'password'; + } + if ($password == '.') { + $password = ""; + } + + $uri = "pgsql://postgres:${password}@${host}/migratedatabase"; + + $this->migrate = new Migration(new Uri($uri), __DIR__ . '/../example/postgres', true, $this->migrationTable); + $this->migrate->registerDatabase("pgsql", PgsqlDatabase::class); parent::setUp(); } } diff --git a/tests/SqlServerDatabaseCustomTest.php b/tests/SqlServerDatabaseCustomTest.php index cbf3666..ab2bc8e 100644 --- a/tests/SqlServerDatabaseCustomTest.php +++ b/tests/SqlServerDatabaseCustomTest.php @@ -1,11 +1,11 @@ migrate = new \ByJG\DbMigration\Migration(new \ByJG\Util\Uri($this->uri), __DIR__ . '/../example/sql_server', true, $this->migrationTable); - $this->migrate->registerDatabase("dblib", \ByJG\DbMigration\Database\DblibDatabase::class); + $host = getenv('MSSQL_TEST_HOST'); + if (empty($host)) { + $host = "127.0.0.1"; + } + $password = getenv('MSSQL_PASSWORD'); + if (empty($password)) { + $password = 'Pa55word'; + } + + $uri = "dblib://sa:${password}@${host}/migratedatabase"; + + $this->migrate = new Migration(new Uri($uri), __DIR__ . '/../example/sql_server', true, $this->migrationTable); + $this->migrate->registerDatabase("dblib", DblibDatabase::class); parent::setUp(); } } diff --git a/tests/SqlServerDblibDatabaseTest.php b/tests/SqlServerDblibDatabaseTest.php new file mode 100644 index 0000000..c01d223 --- /dev/null +++ b/tests/SqlServerDblibDatabaseTest.php @@ -0,0 +1,38 @@ +scheme . "://sa:${password}@${host}/migratedatabase"; + + $this->migrate = new Migration(new Uri($uri), __DIR__ . '/../example/sql_server', true, $this->migrationTable); + $this->migrate->registerDatabase($this->scheme, DblibDatabase::class); + parent::setUp(); + } +} diff --git a/tests/SqlServerSqlsrvDatabaseTest.php b/tests/SqlServerSqlsrvDatabaseTest.php new file mode 100644 index 0000000..48009e5 --- /dev/null +++ b/tests/SqlServerSqlsrvDatabaseTest.php @@ -0,0 +1,19 @@ +scheme = "sqlsrv"; + parent::setUp(); + } +} diff --git a/tests/SqliteDatabaseTest.php b/tests/SqliteDatabaseTest.php index 3e66e80..92467ea 100644 --- a/tests/SqliteDatabaseTest.php +++ b/tests/SqliteDatabaseTest.php @@ -1,5 +1,9 @@ path = getenv('SQLITE_TEST_HOST'); + if (empty($this->path)) { + $this->path = ':memory:'; + } + # Dump SQLite database. $this->prepareDatabase(); - $uri = new \ByJG\Util\Uri("sqlite://{$this->path}"); - $this->migrate = new \ByJG\DbMigration\Migration($uri, __DIR__ . '/../example/sqlite', true, $this->migrationTable); - $this->migrate->registerDatabase("sqlite", \ByJG\DbMigration\Database\SqliteDatabase::class); + $uri = new Uri("sqlite://{$this->path}"); + + $this->migrate = new Migration($uri, __DIR__ . '/../example/sqlite', true, $this->migrationTable); + $this->migrate->registerDatabase("sqlite", SqliteDatabase::class); parent::setUp(); } @@ -31,9 +41,9 @@ public function testUsingCustomTable() $this->prepareDatabase(); - $uri = new \ByJG\Util\Uri("sqlite://{$this->path}"); - $this->migrate = new \ByJG\DbMigration\Migration($uri, __DIR__ . '/../example/sqlite', true, $this->migrationTable); - $this->migrate->registerDatabase("sqlite", \ByJG\DbMigration\Database\SqliteDatabase::class); + $uri = new Uri("sqlite://{$this->path}"); + $this->migrate = new Migration($uri, __DIR__ . '/../example/sqlite', true, $this->migrationTable); + $this->migrate->registerDatabase("sqlite", SqliteDatabase::class); parent::testUpVersion1(); } diff --git a/tests/dirstructure/migrations/up/00001.sql b/tests/dirstructure/migrations/up/00001.sql index e69de29..bba8da9 100644 --- a/tests/dirstructure/migrations/up/00001.sql +++ b/tests/dirstructure/migrations/up/00001.sql @@ -0,0 +1,3 @@ +-- @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 e69de29..3f8ff69 100644 --- a/tests/dirstructure/migrations/up/00002.sql +++ b/tests/dirstructure/migrations/up/00002.sql @@ -0,0 +1,3 @@ +-- @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 e69de29..3f17ec4 100644 --- a/tests/dirstructure/migrations/up/00003.sql +++ b/tests/dirstructure/migrations/up/00003.sql @@ -0,0 +1 @@ +select something from sometable; \ No newline at end of file diff --git a/wait-for-db.sh b/wait-for-db.sh new file mode 100755 index 0000000..44fac12 --- /dev/null +++ b/wait-for-db.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +if [ -z "$1" ] +then + echo "Database is necessary" + exit 1 +fi + +n=0; +max=10 +while [ -z "$(docker ps -q -f health=healthy -f name=anydataset_db_$1)" ] && [ "$n" -lt "$max" ]; +do + echo "Waiting for $1..."; + n=$(( n + 1 )) + sleep 5; +done + +if [ "$n" -gt "$max" ] +then + echo "$mysql was not health after $(( max * 5 ))" + exit 2 +fi + +echo "$1 is up" + +