diff --git a/.codeclimate.yml b/.codeclimate.yml index e712691..6b68128 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -1,48 +1,53 @@ -engines: - duplication: - enabled: true - config: - languages: - - php - fixme: - enabled: true +version: "2" +plugins: + # Disabling plugins until there is a reasonable way to sanitize their output + phpcodesniffer: + enabled: false + config: + standard: "PSR1,PSR2" + ignore_warnings: true + encoding: utf-8 phpmd: - enabled: true - exclude_fingerprints: - checks: - CyclomaticComplexity: - enabled: false - Design/TooManyPublicMethods: - enabled: false - Design/TooManyMethods: - enabled: false - Design/NpathComplexity: - enabled: false - Design/WeightedMethodCount: - enabled: false - Design/LongClass: - enabled: false - Controversial/CamelCaseMethodName: - enabled: false - Controversial/CamelCaseParameterName: - enabled: false - Controversial/CamelCasePropertyName: - enabled: false - Controversial/CamelCaseVariableName: - enabled: false - Controversial/CamelCaseClassName: - enabled: false - Naming/ShortVariable: - enabled: false - CleanCode/ElseExpression: - enabled: false + enabled: false + sonar-php: + enabled: false + +checks: + argument-count: + config: + threshold: 4 + complex-logic: + config: + threshold: 4 + file-lines: + config: + threshold: 1000 + method-complexity: + config: + threshold: 40 + method-count: + config: + threshold: 40 + method-lines: + config: + threshold: 100 + nested-control-flow: + config: + threshold: 4 + return-statements: + config: + threshold: 4 + similar-code: + config: + threshold: 100 + identical-code: + config: + threshold: 150 - radon: - enabled: true ratings: paths: - - src/** -exclude_paths: -- docs/**/* -- tests/**/* -- vendor/**/* + - src/**/* +exclude_patterns: + - docs/**/* + - tests/**/* + - vendor/**/* diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..8cf8ccf --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,9 @@ +# See https://github.com/release-drafter/release-drafter#configuration +categories: + - title: 'Enhancements' + labels: + - enhancement +template: | + ## What’s Changed + + $CHANGES diff --git a/.github/workflows/bundler.yml b/.github/workflows/bundler.yml new file mode 100644 index 0000000..29c092b --- /dev/null +++ b/.github/workflows/bundler.yml @@ -0,0 +1,45 @@ +name: Bundler + +on: create + +jobs: + autocommit: + name: Update to stable dependencies + if: startsWith(github.ref, 'refs/heads/release/') + runs-on: ubuntu-latest + container: + image: atk4/image:latest # https://github.com/atk4/image + steps: + - uses: actions/checkout@master + - run: echo ${{ github.ref }} + - name: Update to stable dependencies + run: | + jq 'del(.require["atk4/dsql"]) | del(.["require-dev"]["atk4/ui"]) | del(.["require-dev"]["atk4/data"])' < composer.json > tmp && mv tmp composer.json + + composer require --no-progress --no-suggest --prefer-dist --optimize-autoloader atk4/dsql + #composer require --dev atk4/data # atk4/ui - removed temporarily until atk4/ui is released + composer update --no-suggest --prefer-dist --optimize-autoloader + + - uses: teaminkling/autocommit@master + with: + commit-message: Setting current dependencies + - uses: ad-m/github-push-action@master + with: + branch: ${{ github.ref }} + github_token: ${{ secrets.GITHUB_TOKEN }} + + - name: pull-request + uses: repo-sync/pull-request@v2 + with: + source_branch: "" # If blank, default: triggered branch + destination_branch: "master" # If blank, default: master + pr_title: "Releasing ${{ github.ref }} into master" + pr_body: | + - [ ] Review changes (must include stable dependencies) + - [ ] Merge this PR into master (will delete ${{ github.ref }}) + - [ ] Go to Releases and create TAG from master + Do not merge master into develop + pr_reviewer: "romaninsh" + pr_assignee: "romaninsh" + github_token: ${{ secrets.GITHUB_TOKEN }} + diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..59fb981 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,16 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - develop + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: toolmantim/release-drafter@v5.2.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..77685ce --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,59 @@ +name: Unit Testing + +on: + pull_request: + branches: '*' + push: + branches: + - master + - develop + +jobs: + unit-test: + name: Unit Testing + runs-on: ubuntu-latest + container: + image: atk4/image:${{ matrix.php }} # https://github.com/atk4/image + strategy: + matrix: + php: ['7.2', '7.3', 'latest'] + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ROOT_PASSWORD: password + DB_DATABASE: dsql_test + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 + steps: + - uses: actions/checkout@v1 + - run: php --version + - name: Get Composer Cache Directory + id: composer-cache + run: | + echo "::set-output name=dir::$(composer config cache-files-dir)" + - uses: actions/cache@v1 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('composer.json') }} + restore-keys: | + ${{ runner.os }}-composer- + + - run: composer install --no-progress --no-suggest --prefer-dist --optimize-autoloader + + - name: Run Tests + run: | + mkdir -p build/logs + mysql -uroot -ppassword -h mysql -e 'CREATE DATABASE dsql_test;' + - name: SQLite Testing + run: vendor/bin/phpunit --configuration phpunit.xml --coverage-text --exclude-group dns + + - name: MySQL Testing + run: vendor/bin/phpunit --configuration phpunit-mysql.xml --exclude-group dns + + - name: Merge coverage logs + run: vendor/bin/phpcov merge build/logs/ --clover build/logs/cc.xml; + + - uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: build/logs/cc.xml diff --git a/.gitignore b/.gitignore index 705c8ff..44d9033 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ docs/build /build /vendor .DS_Store +/.idea/ diff --git a/.old.travis.yml b/.old.travis.yml new file mode 100644 index 0000000..64b4eec --- /dev/null +++ b/.old.travis.yml @@ -0,0 +1,51 @@ +language: php + +php: + - '7.2' + - '7.3' + +cache: + directories: + - $HOME/.composer/cache + +services: + - mysql + +before_script: + - composer self-update + - composer install --no-ansi + - mysql -e 'create database dsql_test;' + - mysql -e 'SET GLOBAL max_connections = 1000;' + - mkdir -p build/logs + +script: + - if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.2" ]]; then CM=""; NC=""; else CM=""; NC="--no-coverage"; fi + - $CM ./vendor/bin/phpunit --configuration phpunit.xml $NC + - $CM ./vendor/bin/phpunit --configuration phpunit-mysql.xml $NC + +after_script: + - if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.2" ]]; then + echo "Merging coverage reports:"; + vendor/bin/phpcov merge build/logs/ --clover build/logs/cc.xml; + echo "We now have these coverage files:"; + ls -l build/logs; + echo "Sending codeclimate report:"; + vendor/bin/test-reporter --coverage-report build/logs/cc.xml; + echo "Sending codecov report:"; + TRAVIS_CMD="" bash <(curl -s https://codecov.io/bash) -f build/logs/cc.xml; + fi + +notifications: + slack: + rooms: + - agiletoolkit:bjrKuPBf1h4cYiNxPBQ1kF6c#dsql + on_success: change + + urls: + - https://webhooks.gitter.im/e/b33a2db0c636f34bafa9 + + on_success: change # options: [always|never|change] default: always + on_failure: always # options: [always|never|change] default: always + on_start: never # options: [always|never|change] default: always + + email: false diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index dec4011..0000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -language: php - -php: - - '5.6' - - '7.0' - - '7.1' - -#services: -# - mysql - -before_script: - - composer install - - mysql -e 'create database test3;' - -after_script: - - echo $TRAVIS_PHP_VERSION - - if [[ ${TRAVIS_PHP_VERSION:0:3} == "7.0" ]]; then echo "Sending coverage report"; vendor/bin/test-reporter; fi - -script: - - ./vendor/phpunit/phpunit/phpunit - -cache: - directories: - - $HOME/.composer/cache diff --git a/CHANGELOG.md b/CHANGELOG.md index 602f1c7..c30e72c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,48 @@ -# 1.1 +# Change Log + +## [1.1.6](https://github.com/atk4/schema/tree/1.1.6) + +[Full Changelog](https://github.com/atk4/schema/compare/1.1.5...1.1.6) + +**Fixed bugs:** + +- Feature/support array and object types [\#12](https://github.com/atk4/schema/pull/12) ([DarkSide666](https://github.com/DarkSide666)) + +**Merged pull requests:** + +- compatibility with new data namespace [\#11](https://github.com/atk4/schema/pull/11) ([DarkSide666](https://github.com/DarkSide666)) + +## [1.1.5](https://github.com/atk4/schema/tree/1.1.5) (2018-08-16) + +[Full Changelog](https://github.com/atk4/schema/compare/1.1.4...1.1.5) + +**Merged pull requests:** + +- fix alter and drop field [\#7](https://github.com/atk4/schema/pull/7) ([DarkSide666](https://github.com/DarkSide666)) + +## [1.1.4](https://github.com/atk4/schema/tree/1.1.4) (2018-04-19) + +[Full Changelog](https://github.com/atk4/schema/compare/1.1.3...1.1.4) + +**Merged pull requests:** + +- Feature/add migrator [\#6](https://github.com/atk4/schema/pull/6) ([romaninsh](https://github.com/romaninsh)) + +## [1.1.3](https://github.com/atk4/schema/tree/1.1.3) (2018-04-10) + +[Full Changelog](https://github.com/atk4/schema/compare/1.1.2...1.1.3) + +## [1.1.2](https://github.com/atk4/schema/tree/1.1.2) (2018-04-06) + +[Full Changelog](https://github.com/atk4/schema/compare/1.1.1...1.1.2) + +## [1.1.1](https://github.com/atk4/schema/tree/1.1.1) (2018-04-06) + +[Full Changelog](https://github.com/atk4/schema/compare/1.1.0...1.1.1) + +## [1.1.0](https://github.com/atk4/schema/tree/1.1.0) (2018-04-03) + +[Full Changelog](https://github.com/atk4/schema/compare/1.0.2...1.1.0) **Closed issues:** @@ -10,19 +54,26 @@ - Feature/model integration [\#4](https://github.com/atk4/schema/pull/4) ([romaninsh](https://github.com/romaninsh)) - drop php 5.5, update phpunit [\#3](https://github.com/atk4/schema/pull/3) ([DarkSide666](https://github.com/DarkSide666)) -## 1.0.2 +## [1.0.2](https://github.com/atk4/schema/tree/1.0.2) (2017-04-12) + +[Full Changelog](https://github.com/atk4/schema/compare/1.0.1...1.0.2) + +## [1.0.1](https://github.com/atk4/schema/tree/1.0.1) (2017-04-12) -Cleanup dependencies +[Full Changelog](https://github.com/atk4/schema/compare/1.0...1.0.1) -## 1.0.1 +## [1.0](https://github.com/atk4/schema/tree/1.0) (2016-09-30) -Added release script +[Full Changelog](https://github.com/atk4/schema/compare/0.1...1.0) -## 1.0.0 +## [0.1](https://github.com/atk4/schema/tree/0.1) (2016-09-30) + +[Full Changelog](https://github.com/atk4/schema/compare/046cc18d3f924ec52cc4959a2d8195572ddb22c8...0.1) + +**Merged pull requests:** -Initial release with working Migrator and PHPUnit schema +- Applied fixes from StyleCI [\#1](https://github.com/atk4/schema/pull/1) ([romaninsh](https://github.com/romaninsh)) -## 0.1.0 -* Initial Release +\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* diff --git a/README.md b/README.md index d2e8a81..01200c3 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,70 @@ -# Agile Data - Schema Add-on +# Agile Data - SQL Schema Management Add-on -This extension for Agile Data implements ability to work with SQL schema, -execute migrations, perform DB-tests on specific structures - -Code Quality: +This extension for Agile Data implements ability to work with SQL schema, execute migrations, perform DB-tests in PHPUnit (used by other ATK frameworks) and sync up "Model" structure to the database. [![Build Status](https://travis-ci.org/atk4/schema.png?branch=develop)](https://travis-ci.org/atk4/schema) [![Code Climate](https://codeclimate.com/github/atk4/schema/badges/gpa.svg)](https://codeclimate.com/github/atk4/schema) [![StyleCI](https://styleci.io/repos/69662508/shield)](https://styleci.io/repos/69662508) -[![Test Coverage](https://codeclimate.com/github/atk4/schema/badges/coverage.svg)](https://codeclimate.com/github/atk4/schema) +[![CodeCov](https://codecov.io/gh/atk4/schema/branch/develop/graph/badge.svg)](https://codecov.io/gh/atk4/schema) +[![Test Coverage](https://codeclimate.com/github/atk4/schema/badges/coverage.svg)](https://codeclimate.com/github/atk4/schema/coverage) +[![Issue Count](https://codeclimate.com/github/atk4/schema/badges/issue_count.svg)](https://codeclimate.com/github/atk4/schema) + +[![License](https://poser.pugx.org/atk4/schema/license)](https://packagist.org/packages/atk4/schema) +[![GitHub release](https://img.shields.io/github/release/atk4/schema.svg?maxAge=2592000)](CHANGELOG.md) + + +### Basic Usage: + +``` php +// Add the following code on your setup page / wizard: + +$app->add('MigratorConsole') + ->migrateModels([ + new Model\User($app->db), + new Model\Order($app->db), + new Model\Payment($app->db) + ]); +``` + +The user will see a console which would adjust database to contain required tables / fields for the models: -Resources and Community: +![migrator-console](docs/migrator-console.png) -[![Documentation Status](https://readthedocs.org/projects/agile-schema/badge/?version=develop)](http://agile-schema.readthedocs.io/en/develop/?badge=latest) -[![Gitter](https://img.shields.io/gitter/room/atk4/atk4.svg?maxAge=2592000)](https://gitter.im/atk4/atk4?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -[![Stack Overlfow Community](https://img.shields.io/stackexchange/stackoverflow/t/atk4.svg?maxAge=2592000)](http://stackoverflow.com/questions/ask?tags=atk4) -[![Discord User forum](https://img.shields.io/badge/discord-User_Forum-green.svg)](https://forum.agiletoolkit.org/c/44) +Of course it's also possible to perform migration without visual feedback: + +``` php +$changes = (\atk4\schema\Migration::getMigration(new User($app->db)))->migrate(); +``` -Stats: +If you need a more fine-graned migration, you can define them in great detail. -[![GitHub release](https://img.shields.io/github/release/atk4/schema.svg)](CHANGELOG.md) +``` php +// create table +$migration = \atk4\schema\Migration::getMigration($app->db); +$migration->table('user') + ->id() + ->field('name') + ->field('address', ['type'=>'text']); + ->create(); + +// or alter +$migration = \atk4\schema\Migration::getMigration($app->db); +$migration->table('user') + ->newField('age', ['type'=>'integer']) + ->alter(); +``` +Currently we fully support MySQL and SQLite connections, partly PgSQL and Oracle connections. Other SQL databases are not yet supported. +Field declaration uses same types as [ATK Data](https://github.com/atk4/data). -## Example +## Examples `schema\Migration` is a simple class for building schema-related queries using DSQL. ``` php table('user')->drop(); $m->field('id'); $m->field('name', ['type'=>'string']); @@ -38,7 +73,7 @@ $m->field('bio'); $m->create(); ``` -`schema\Snapshot` is a simple class that can record and restore +`schema\Snapshot` (NOT IMPLEMENTED) is a simple class that can record and restore table contents: ``` php @@ -51,20 +86,6 @@ $tables = $s->getDB($tables); $s->setDB($tables); ``` -`schema\AutoCreator` is a simple class reads model and decides -if any changes to the database are needed. Will create a -necessary schema\Migration which you can execute. - -``` php - true]); -$a->compare()->execute(); -``` - ## Integration with PHPUnit You can now automate your database testing by setting and checking your @@ -92,7 +113,7 @@ against any other state. - Automatically add 'id' field by default - Create tables for you -- Detect types (int, string, etc) +- Detect types (int, string, date, boolean etc) - Hides ID values if you don't pass them ## Installation diff --git a/composer.json b/composer.json index 46e687d..c3816b9 100644 --- a/composer.json +++ b/composer.json @@ -1,37 +1,50 @@ { - "name": "atk4/schema", - "type": "library", - "description": "Agile Schema", - "keywords": ["agile", "schema", "data", "migration", "alter", "sql", "framework"], - "homepage": "http://github.com/atk4/schema", - "license": "MIT", - "authors": [ - { - "name": "Romans Malinovskis", - "email": "romans@agiletoolkit.org", - "homepage": "https://nearly.guru/" - } - ], - "require": { - "php": ">=5.6.0", - "atk4/dsql": "dev-develop", - "atk4/core": "dev-develop" - }, - "suggest": { - "atk4/data": "*", - "jdorn/sql-formatter": "*" - }, - "require-dev": { - "phpunit/phpunit": "<6", - "atk4/data": "dev-develop", - "codeclimate/php-test-reporter": "*" - }, - "autoload": { - "psr-4": {"atk4\\schema\\":"src/"} - }, - "autoload-dev": { - "psr-4": { - "atk4\\schema\\tests\\":"tests/" - } + "name": "atk4/schema", + "type": "library", + "description": "Agile Schema", + "keywords": [ + "agile", + "schema", + "data", + "migration", + "alter", + "sql", + "framework" + ], + "homepage": "http://github.com/atk4/schema", + "license": "MIT", + "minimum-stability": "dev", + "prefer-stable": true, + "authors": [ + { + "name": "Romans Malinovskis", + "email": "romans@agiletoolkit.org", + "homepage": "https://nearly.guru/" } + ], + "require": { + "php": ">=7.2.0", + "atk4/dsql": "^2.0" + }, + "suggest": { + "atk4/data": "*", + "atk4/ui": "*", + "jdorn/sql-formatter": "*" + }, + "require-dev": { + "phpunit/phpunit": "<6", + "phpunit/dbunit": ">=1.2", + "phpunit/phpcov": "*", + "codeclimate/php-test-reporter": "*" + }, + "autoload": { + "psr-4": { + "atk4\\schema\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "atk4\\schema\\tests\\": "tests/" + } + } } diff --git a/demos/init.php b/demos/init.php index e9736b7..1634aee 100644 --- a/demos/init.php +++ b/demos/init.php @@ -1,4 +1,5 @@ addField('name'); @@ -13,14 +16,12 @@ function init() { try { // apply migrator - (new \atk4\schema\Migration\MySQL($m))->migrate(); - + (\atk4\schema\Migration::getMigration($m))->migrate(); // ok, now we surely have DB! - $m->save([ - 'name'=>'John'.rand(1,100) + 'name'=> 'John'.rand(1, 100), ]); } catch (\atk4\core\Exception $e) { echo $e->getColorfulText(); diff --git a/docs/migrator-console.png b/docs/migrator-console.png new file mode 100644 index 0000000..d40a9bc Binary files /dev/null and b/docs/migrator-console.png differ diff --git a/phpunit-mysql.xml b/phpunit-mysql.xml new file mode 100644 index 0000000..4986a6e --- /dev/null +++ b/phpunit-mysql.xml @@ -0,0 +1,24 @@ + + + + + + + + + + ./vendor + + + ./src + + + + + tests + + + + + + diff --git a/phpunit.xml b/phpunit.xml index 40b70f9..4b15cdd 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,6 +1,11 @@ - + @@ -13,10 +18,9 @@ tests - tests/smbo/lib - + diff --git a/src/Migration.php b/src/Migration.php index 4563282..13c1872 100644 --- a/src/Migration.php +++ b/src/Migration.php @@ -3,10 +3,14 @@ namespace atk4\schema; use atk4\core\Exception; +use atk4\data\Field_SQL_Expression; +use atk4\Data\Model; +use atk4\data\Persistence; +use atk4\data\Reference\HasOne; +use atk4\dsql\Connection; use atk4\dsql\Expression; -use atk4\dsql\Expression_MySQL; -class Migration extends Expression_MySQL +class Migration extends Expression { /** @var string Expression mode. See $templates. */ public $mode = 'create'; @@ -16,67 +20,175 @@ class Migration extends Expression_MySQL 'create' => 'create table {table} ([field])', 'drop' => 'drop table if exists {table}', 'alter' => 'alter table {table} [statements]', + 'rename' => 'rename table {old_table} to {table}', ]; - /** @var \atk4\dsql\Connection Database connection */ + /** @var Connection Database connection */ public $connection; + /** + * Field, table and alias name escaping symbol. + * By SQL Standard it's double quote, but MySQL uses backtick. + * + * @var string + */ + protected $escape_char = '"'; + /** @var string Expression to create primary key */ public $primary_key_expr = 'integer primary key autoincrement'; + /** @var array Conversion mapping from Agile Data types to persistence types */ + protected $defaultMapToPersistence = [ + ['varchar', 255], // default + 'boolean' => ['tinyint', 1], + 'integer' => ['int'], + 'money' => ['decimal', 12, 2], + 'float' => ['decimal', 16, 6], + 'date' => ['date'], + 'datetime' => ['datetime'], + 'time' => ['varchar', 8], + 'text' => ['text'], + 'array' => ['text'], + 'object' => ['text'], + ]; + + /** @var array use this array in extended classes to overwrite or extend values of default mapping */ + public $mapToPersistence = []; + + /** @var array Conversion mapping from persistence types to Agile Data types */ + protected $defaultMapToAgile = [ + [null], // default + 'tinyint' => ['boolean'], + 'int' => ['integer'], + 'decimal' => ['float'], + 'numeric' => ['float'], + 'date' => ['date'], + 'datetime' => ['datetime'], + 'timestamp' => ['datetime'], + 'text' => ['text'], + ]; + + /** @var array use this array in extended classes to overwrite or extend values of default mapping */ + public $mapToAgile = []; + /** - * Create new migration. + * Factory method to get correct Migration subclass object depending on connection given. * - * @param \atk4\dsql\Connection|\atk4\data\Persistence|\atk4\data\Model $source - * @param array $params + * @param Connection|Persistence|Model $source + * @param array $params + * + * @throws Exception + * + * @return Migration Subclass */ - public function __construct($source, $params = []) + public static function getMigration($source, $params = []) : self { - parent::__construct($params); + $c = static::getConnection($source); + + switch ($c->driver) { + case 'sqlite': + return new Migration\SQLite($source, $params); + case 'mysql': + return new Migration\MySQL($source, $params); + case 'pgsql': + return new Migration\PgSQL($source, $params); + case 'oci': + return new Migration\Oracle($source, $params); + default: + throw new Exception([ + 'Not sure which migration class to use for your DSN', + 'driver' => $c->driver, + 'source' => $source, + ]); + } + } - if ($source instanceof \atk4\dsql\Connection) { - $this->connection = $source; - return; - } elseif ($source instanceof \atk4\data\Persistence_SQL) { - $this->connection = $source->connection; - return; - } elseif ($source instanceof \atk4\data\Model) { - if ($source->persistence && $source->persistence instanceof \atk4\data\Persistence_SQL) { - $this->connection = $source->persistence->connection; - - $this->setModel($source); - return; - } + /** + * Static method to extract DB driver from Connection, Persistence or Model. + * + * @param Connection|Persistence|Model $source + * + * @throws Exception + * + * @return Connection + */ + public static function getConnection($source) : Connection + { + if ($source instanceof Connection) { + return $source; + } elseif ($source instanceof Persistence\SQL) { + return $source->connection; + } elseif ( + $source instanceof Model + && $source->persistence + && ($source->persistence instanceof Persistence\SQL) + ) { + return $source->persistence->connection; } - throw new \atk4\core\Exception([ + throw new Exception([ 'Source is specified incorrectly. Must be Connection, Persistence or initialized Model', 'source' => $source, ]); } + /** + * Create new migration. + * + * @param Connection|Persistence|Model $source + * @param array $params + * + * @throws Exception + * @throws \atk4\dsql\Exception + */ + public function __construct($source, $params = []) + { + parent::__construct($params); + + $this->setSource($source); + } + + /** + * Sets source of migration. + * + * @param Connection|Persistence|Model $source + * + * @throws Exception + */ + public function setSource($source) + { + $this->connection = static::getConnection($source); + + if ( + $source instanceof Model + && $source->persistence + && ($source->persistence instanceof Persistence\SQL) + ) { + $this->setModel($source); + } + } + /** * Sets model. * - * @param \atk4\data\Model $m + * @param Model $m * - * @return \atk4\data\Model + * @throws Exception + * @throws \ReflectionException + * + * @return Model */ - public function setModel(\atk4\data\Model $m) + public function setModel(Model $m) :Model { $this->table($m->table); - foreach($m->elements as $field) { + foreach ($m->getFields() as $field) { // ignore not persisted model fields - if (!$field instanceof \atk4\data\Field) { - continue; - } - if ($field->never_persist) { continue; } - if ($field instanceof \atk4\data\Field_SQL_Expression) { + if ($field instanceof Field_SQL_Expression) { continue; } @@ -85,7 +197,37 @@ public function setModel(\atk4\data\Model $m) continue; } - $this->field($field->actual ?: $field->short_name); // todo add options here + // get field type from field + $type = $field->type; + + // if the field is a hasOne relation + // Don't have the right FieldType + // FieldType is stored in the reference field + if ($field->reference instanceof HasOne) { + + // @TODO if this can be done better? + + // i don't want to : + // - change the isolation of relation link + // - expose the protected property ->their_field + // i need the type of the field to be used in this table + $reflection = new \ReflectionClass($field->reference); + $property = $reflection->getProperty('their_field'); + $property->setAccessible(true); + + /** @var string $reference_their_field get Reflection protected property Reference->their_field */ + $reference_their_field = $property->getValue($field->reference); + + /** @var string $reference_field reference field name */ + $reference_field = $reference_their_field ?? $field->reference->owner->id_field; + + /** @var string $reference_model_class reference class fqcn */ + $reference_model_class = $field->reference->model; + + $type = (new $reference_model_class($m->persistence))->getField($reference_field)->type ?? 'integer'; + } + + $this->field($field->actual ?: $field->short_name, ['type' => $type]); // todo add more options here } return $m; @@ -96,9 +238,11 @@ public function setModel(\atk4\data\Model $m) * * @param string $mode Template name * + * @throws Exception + * * @return $this */ - public function mode($mode) + public function mode(string $mode) :self { if (!isset($this->templates[$mode])) { throw new Exception(['Structure builder does not have this mode', 'mode' => $mode]); @@ -113,9 +257,12 @@ public function mode($mode) /** * Create new table. * + * @throws Exception + * @throws \atk4\dsql\Exception + * * @return $this */ - public function create() + public function create() :self { $this->mode('create')->execute(); @@ -125,9 +272,12 @@ public function create() /** * Drop table. * + * @throws Exception + * @throws \atk4\dsql\Exception + * * @return $this */ - public function drop() + public function drop() :self { $this->mode('drop')->execute(); @@ -137,33 +287,43 @@ public function drop() /** * Alter table. * + * @throws Exception + * @throws \atk4\dsql\Exception + * * @return $this */ - public function alter() + public function alter() :self { $this->mode('alter')->execute(); return $this; } + /** + * Rename table. + * + * @throws Exception + * @throws \atk4\dsql\Exception + * + * @return $this + */ + public function rename() :self + { + $this->mode('rename')->execute(); - - - - - - - - + return $this; + } /** * Will read current schema and consult current 'field' arguments, to see if they are matched. * If table does not exist, will invoke ->create. If table does exist, then it will execute - * methods ->addColumn(), ->dropColumn() or ->updateColumn() as needed, then call ->alter() + * methods ->newField(), ->dropField() or ->alterField() as needed, then call ->alter(). + * + * @throws Exception * * @return string Returns short textual info for logging purposes */ - public function migrate() + public function migrate() :string { $changes = $added = $altered = $dropped = 0; @@ -173,40 +333,57 @@ public function migrate() if (!$migration2->importTable($this['table'])) { // should probably use custom exception class here $this->create(); + return 'created new table'; } $old = $migration2->_getFields(); $new = $this->_getFields(); + // add new fields or update existing ones foreach ($new as $field => $options) { + // never update ID field (sadly hard-coded field name) if ($field == 'id') { continue; } if (isset($old[$field])) { - // todo - compare options and if needed, call - //$this->alterField($field, $options); + + // compare options and if needed alter field + // @todo add more options here like 'len' + if (array_key_exists('type', $old[$field]) && array_key_exists('type', $options) && $old[$field]['type'] != $options['type']) { + $this->alterField($field, $options); + $altered++; + $changes++; + } + unset($old[$field]); } else { - // new field, so + // new field, so let's just add it $this->newField($field, $options); $added++; $changes++; } } - // remaining fields + // remaining old fields - drop them foreach ($old as $field => $options) { + // never delete ID field (sadly hard-coded field name) if ($field == 'id') { continue; } - //$this->dropField($field); + + $this->dropField($field); + $dropped++; + $changes++; } - if($changes) { + if ($changes) { $this->alter(); - return 'added '.$added.' field'.($added%10==1?'':'s').' and changed '.$altered; + + return 'added '.$added.' field'.($added % 10 == 1 ? '' : 's').', '. + 'changed '.$altered.' field'.($altered % 10 == 1 ? '' : 's').' and '. + 'deleted '.$dropped.' field'.($dropped % 10 == 1 ? '' : 's'); } return 'no changes'; @@ -217,50 +394,57 @@ public function migrate() * * @return string */ - public function _render_statements() + public function _render_statements() :string { $result = []; if (isset($this->args['dropField'])) { - foreach($this->args['dropField'] as $field => $junk) { - $result[] = 'drop column '. $this->_escape($field); + foreach ($this->args['dropField'] as $field => $junk) { + $result[] = 'drop column '.$this->_escape($field); } } if (isset($this->args['newField'])) { - foreach($this->args['newField'] as $field => $option) { - $result[] = 'add column '. $this->_render_one_field($field, $option); + foreach ($this->args['newField'] as $field => $option) { + $result[] = 'add column '.$this->_render_one_field($field, $option); } } if (isset($this->args['alterField'])) { - foreach($this->args['alterField'] as $field => $option) { - $result[] = 'change column '. $this->_escape($field). ' '. $this->_render_one_field($field, $option); + foreach ($this->args['alterField'] as $field => $option) { + $result[] = 'change column '.$this->_escape($field).' '.$this->_render_one_field($field, $option); } } - return join(', ', $result); + return implode(', ', $result); } - /** * Create rough model from current set of $this->args['fields']. This is not * ideal solution but is designed as a drop-in solution. * - * @param \atk4\data\Persistence $persistence - * @param string $table + * @param Persistence $persistence + * @param string $table + * + * @throws Exception + * @throws \atk4\data\Exception * - * @return \atk4\data\Model + * @return Model */ - public function createModel($persistence, $table = null) + public function createModel($persistence, $table = null) : Model { - $m = new \atk4\data\Model([$persistence, 'table'=>$table ?: $this['table'] = $table]); + $this['table'] = $table ?? $this['table']; - foreach ($this->_getFields() as $field => $options) { + $m = new Model([$persistence, 'table'=> $this['table']]); - if($field=='id')continue; + $this->importTable($this['table']); - if(is_object($options)) { + foreach ($this->_getFields() as $field => $options) { + if ($field == 'id') { + continue; + } + + if (is_object($options)) { continue; } @@ -281,9 +465,11 @@ public function createModel($persistence, $table = null) * @param string $field * @param array $options * + * @throws Exception + * * @return $this */ - public function newField($field, $options = []) + public function newField($field, $options = []) :self { $this->_set_args('newField', $field, $options); @@ -293,16 +479,17 @@ public function newField($field, $options = []) /** * Sets alterField argument. * - * Note: can not rename fields - * * @param string $field * @param array $options * + * @throws Exception + * * @return $this */ - public function alterField($field, $options = []) + public function alterField(string $field, $options = []) :self { $this->_set_args('alterField', $field, $options); + return $this; } @@ -311,79 +498,89 @@ public function alterField($field, $options = []) * * @param string $field * + * @throws Exception + * * @return $this */ - public function dropField($field) + public function dropField($field) :self { $this->_set_args('dropField', $field, true); return $this; } - /** * Return database table descriptions. * DB engine specific. * - * @todo Convert to abstract function + * @todo Maybe convert to abstract function * * @param string $table * * @return array */ - public function describeTable($table) { + public function describeTable(string $table) : array + { return $this->connection->expr('pragma table_info({})', [$table])->get(); } + /** + * Convert SQL field types to Agile Data field types. + * + * @param string $type SQL field type + * + * @return string|null + */ + public function getModelFieldType(string $type) :?string + { + // remove parenthesis + $type = trim(preg_replace('/\(.*/', '', strtolower($type))); + + $map = array_replace($this->defaultMapToAgile, $this->mapToAgile); + $a = array_key_exists($type, $map) ? $map[$type] : $map[0]; + + return $a[0]; + } + + /** + * Convert Agile Data field types to SQL field types. + * + * @param string $type Agile Data field type + * @param array $options More options + * + * @return string|null + */ + public function getSQLFieldType(?string $type, ?array $options = null) :?string + { + $type = strtolower($type); + + $map = array_merge($this->defaultMapToPersistence, $this->mapToPersistence); + $a = array_key_exists($type, $map) ? $map[$type] : $map[0]; + + return $a[0].(count($a) > 1 ? ' ('.implode(',', array_slice($a, 1)).')' : ''); + } + /** * Import fields from database into migration field config. * * @param string $table * + * @throws Exception + * * @return bool */ - public function importTable($table) + public function importTable(string $table) :bool { $this->table($table); $has_fields = false; - foreach($this->describeTable($table) as $row) { + foreach ($this->describeTable($table) as $row) { $has_fields = true; if ($row['pk']) { $this->id($row['name']); continue; } - $type = $row['type']; - if (substr($type, 0,7) == 'varchar') { - $type = null; - } - - if (substr($type, 0,4) == 'char') { - $type = null; - } - if (substr($type, 0,4) == 'enum') { - $type = null; - } - - if ($type == 'int') { - $type = 'integer'; - } - - if ($type == 'decimal') { - $type = 'integer'; - } - - if ($type == 'tinyint') { - $type = 'boolean'; - } - - if ($type == 'longtext') { - $type = 'text'; - } - - if ($type == 'longblob') { - $type = 'text'; - } + $type = $this->getModelFieldType($row['type']); $this->field($row['name'], ['type'=>$type]); } @@ -392,7 +589,7 @@ public function importTable($table) } /** - * Sets table. + * Sets table name. * * @param string $table * @@ -405,12 +602,28 @@ public function table($table) return $this; } + /** + * Sets old table name. + * + * @param string $table + * + * @return $this + */ + public function old_table($old_table) + { + $this['old_table'] = $old_table; + + return $this; + } + /** * Add field in template. * * @param string $name * @param array $options * + * @throws Exception + * * @return $this */ public function field($name, $options = []) @@ -434,7 +647,7 @@ public function id($name = null) $name = 'id'; } - $val = $this->expr($this->primary_key_expr); + $val = $this->connection->expr($this->primary_key_expr); $this->args['field'] = [$name => $val] + (isset($this->args['field']) ? $this->args['field'] : []); @@ -445,6 +658,9 @@ public function id($name = null) /** * Render "field" template. * + * @throws Exception + * @throws \atk4\dsql\Exception + * * @return string */ public function _render_field() @@ -477,18 +693,12 @@ public function _render_field() * * @return string */ - protected function _render_one_field($field, $options) + protected function _render_one_field(string $field, array $options) :string { - $type = strtolower(isset($options['type']) ? - $options['type'] : 'varchar'); - $type = preg_replace('/[^a-z0-9]+/', '', $type); + $name = $options['name'] ?? $field; + $type = $this->getSQLFieldType($options['type'] ?? null, $options); - $len = isset($options['len']) ? - $options['len'] : - ($type === 'varchar' ? 255 : null); - - return $this->_escape($field).' '.$type. - ($len ? ('('.$len.')') : ''); + return $this->_escape($name).' '.$type; } /** @@ -496,7 +706,7 @@ protected function _render_one_field($field, $options) * * @return array */ - public function _getFields() + public function _getFields() :array { return $this->args['field']; } @@ -507,8 +717,10 @@ public function _getFields() * @param string $what Where to set it - table|field * @param string $alias Alias name * @param mixed $value Value to set in args array + * + * @throws Exception */ - protected function _set_args($what, $alias, $value) + protected function _set_args(string $what, string $alias, $value) { // save value in args if ($alias === null) { diff --git a/src/Migration/MySQL.php b/src/Migration/MySQL.php index 46fc371..7e8664b 100644 --- a/src/Migration/MySQL.php +++ b/src/Migration/MySQL.php @@ -4,9 +4,31 @@ class MySQL extends \atk4\schema\Migration { + /** + * Field, table and alias name escaping symbol. + * By SQL Standard it's double quote, but MySQL uses backtick. + * + * @var string + */ + protected $escape_char = '`'; + /** @var string Expression to create primary key */ public $primary_key_expr = 'integer primary key auto_increment'; + /** @var array use this array in extended classes to overwrite or extend values of default mapping */ + public $mapToPersistence = [ + 'text' => ['longtext'], + 'array' => ['longtext'], + 'object' => ['longtext'], + ]; + + /** @var array use this array in extended classes to overwrite or extend values of default mapping */ + public $mapToAgile = [ + 0 => ['string'], + 'longtext' => ['text'], + 'longblob' => ['text'], + ]; + /** * Return database table descriptions. * DB engine specific. @@ -15,7 +37,8 @@ class MySQL extends \atk4\schema\Migration * * @return array */ - public function describeTable($table) { + public function describeTable(string $table) : array + { if (!$this->connection->expr('show tables like []', [$table])->get()) { return []; // no such table } @@ -26,7 +49,7 @@ public function describeTable($table) { $row2 = []; $row2['name'] = $row['Field']; $row2['pk'] = $row['Key'] == 'PRI'; - $row2['type'] = preg_replace('/\(.*/','', $row['Type']); + $row2['type'] = preg_replace('/\(.*/', '', $row['Type']); $result[] = $row2; } diff --git a/src/Migration/Oracle.php b/src/Migration/Oracle.php new file mode 100644 index 0000000..0de8ec6 --- /dev/null +++ b/src/Migration/Oracle.php @@ -0,0 +1,20 @@ + ['date'], + 'datetime' => ['date'], // in Oracle DATE data type is actually datetime + ]; + + /** @var array use this array in extended classes to overwrite or extend values of default mapping */ + public $mapToAgile = [ + 'date' => ['datetime'], + ]; +} diff --git a/src/Migration/PgSQL.php b/src/Migration/PgSQL.php new file mode 100644 index 0000000..6821f8e --- /dev/null +++ b/src/Migration/PgSQL.php @@ -0,0 +1,90 @@ + ['boolean'], + 'date' => ['date'], + 'datetime' => ['timestamp'], // without timezone + 'time' => ['time'], // without timezone + ]; + + /** @var array use this array in extended classes to overwrite or extend values of default mapping */ + public $mapToAgile = [ + 'boolean' => ['boolean'], + 'date' => ['date'], + 'datetime' => ['datetime'], + 'timestamp' => ['datetime'], + 'time' => ['time'], + ]; + + /** + * Return database table descriptions. + * DB engine specific. + * + * @param string $table + * + * @return array + */ + public function describeTable(string $table) : array + { + $columns = $this->connection->expr('SELECT * FROM information_schema.COLUMNS WHERE TABLE_NAME = []', [$table])->get(); + + if (!$columns) { + return []; // no such table + } + + $result = []; + + foreach ($columns as $row) { + $row2 = []; + $row2['name'] = $row['column_name']; + $row2['pk'] = $row['is_identity'] == 'YES'; + $row2['type'] = preg_replace('/\(.*/', '', $row['udt_name']); // $row['data_type'], but it's PgSQL specific type + + $result[] = $row2; + } + + return $result; + } + + /** + * Renders statement. + * + * @return string + */ + public function _render_statements() :string + { + $result = []; + + if (isset($this->args['dropField'])) { + foreach ($this->args['dropField'] as $field => $junk) { + $result[] = 'drop column '.$this->_escape($field); + } + } + + if (isset($this->args['newField'])) { + foreach ($this->args['newField'] as $field => $option) { + $result[] = 'add column '.$this->_render_one_field($field, $option); + } + } + + if (isset($this->args['alterField'])) { + foreach ($this->args['alterField'] as $field => $option) { + $type = $this->getSQLFieldType($option['type'] ?? null, $option); + $result[] = 'alter column '.$this->_escape($field). + ' type '.$type. + ' using ('.$this->_escape($field).'::'.$type.')'; // requires to cast value + } + } + + return implode(', ', $result); + } +} diff --git a/src/Migration/SQLite.php b/src/Migration/SQLite.php index 446878f..e92bb78 100644 --- a/src/Migration/SQLite.php +++ b/src/Migration/SQLite.php @@ -7,6 +7,10 @@ class SQLite extends \atk4\schema\Migration /** @var string Expression to create primary key */ public $primary_key_expr = 'integer primary key autoincrement'; + public $mapToAgile = [ + 0 => ['string'], + ]; + /** * Return database table descriptions. * DB engine specific. @@ -15,7 +19,8 @@ class SQLite extends \atk4\schema\Migration * * @return array */ - public function describeTable($table) { + public function describeTable(string $table) : array + { return $this->connection->expr('pragma table_info({})', [$table])->get(); } } diff --git a/src/MigratorConsole.php b/src/MigratorConsole.php new file mode 100644 index 0000000..ee08a4d --- /dev/null +++ b/src/MigratorConsole.php @@ -0,0 +1,41 @@ +set(function ($c) use ($models) { + $c->notice('Preparing to migrate models'); + $p = $c->app->db; + + foreach ($models as $model) { + if (!is_object($model)) { + $model = $this->factory($model); + $p->add($model); + } + + $m = new $this->migrator_class($model); + $result = $m->migrate(); + + $c->debug(' '.get_class($model).'.. '.$result); + } + + $c->notice('Done with migration'); + }); + } +} diff --git a/src/PHPUnit_SchemaTestCase.php b/src/PHPUnit_SchemaTestCase.php index 3fd44d0..8f6df58 100644 --- a/src/PHPUnit_SchemaTestCase.php +++ b/src/PHPUnit_SchemaTestCase.php @@ -2,8 +2,11 @@ namespace atk4\schema; +use atk4\data\Model; use atk4\data\Persistence; +use atk4\dsql\Connection; +// NOTE: This class should stay here in this namespace because other repos rely on it. For example, atk4\data tests class PHPUnit_SchemaTestCase extends \atk4\core\PHPUnit_AgileTestCase { /** @var \atk4\data\Persistence Persistence instance */ @@ -15,6 +18,9 @@ class PHPUnit_SchemaTestCase extends \atk4\core\PHPUnit_AgileTestCase /** @var bool Debug mode enabled/disabled. In debug mode will use Dumper persistence */ public $debug = false; + /** @var string DSN string */ + protected $dsn; + /** @var string What DB driver we use - mysql, sqlite, pgsql etc */ public $driver = 'sqlite'; @@ -26,41 +32,31 @@ public function setUp() parent::setUp(); // establish connection - $dsn = getenv('DSN'); - if ($dsn) { - $this->db = Persistence::connect(($this->debug ? ('dumper:') : '').$dsn); - list($this->driver, $junk) = explode(':', $dsn, 2); - $this->driver = strtolower($this->driver); - } else { - $this->db = Persistence::connect(($this->debug ? ('dumper:') : '').'sqlite::memory:'); - } + $this->dsn = ($this->debug ? ('dumper:') : '').(isset($GLOBALS['DB_DSN']) ? $GLOBALS['DB_DSN'] : 'sqlite::memory:'); + $user = isset($GLOBALS['DB_USER']) ? $GLOBALS['DB_USER'] : null; + $pass = isset($GLOBALS['DB_PASSWD']) ? $GLOBALS['DB_PASSWD'] : null; + + $this->db = Persistence::connect($this->dsn, $user, $pass); + $this->driver = $this->db->connection->driver; + } + + public function tearDown() + { + unset($this->db); + + parent::tearDown(); // TODO: Change the autogenerated stub } /** * Create and return appropriate Migration object. * - * @param \atk4\dsql\Connection|\atk4\data\Persistence|\atk4\data\Model $m + * @param Connection|Persistence|Model $m * * @return Migration */ public function getMigration($m = null) { - switch ($this->driver) { - case 'sqlite': - return new \atk4\schema\Migration\SQLite($m ?: $this->db); - case 'mysql': - return new \atk4\schema\Migration\MySQL($m ?: $this->db); - //case 'pgsql': - // return new \atk4\schema\Migration\PgSQL($m ?: $this->db); - //case 'oci': - // return new \atk4\schema\Migration\Oracle($m ?: $this->db); - default: - throw new \atk4\core\Exception([ - 'Not sure which migration class to use for your DSN', - 'driver' => $this->driver, - 'dsn' => getenv('DSN'), - ]); - } + return \atk4\schema\Migration::getMigration($m ?: $this->db); } /** @@ -71,7 +67,7 @@ public function getMigration($m = null) */ public function dropTable($table) { - $this->db->connection->expr("drop table if exists {}", [$table])->execute(); + $this->getMigration()->table($table)->drop(); } /** diff --git a/tests/BasicTest.php b/tests/BasicTest.php index 97c5255..e296978 100644 --- a/tests/BasicTest.php +++ b/tests/BasicTest.php @@ -1,10 +1,10 @@ getMigration(); $m->table('user')->id() ->field('foo') - ->field('bar', ['type'=>'integer']) - ->field('baz', ['type'=>'text']) + ->field('bar', ['type' => 'integer']) + ->field('baz', ['type' => 'text']) + ->field('bl', ['type' => 'boolean']) + ->field('tm', ['type' => 'time']) + ->field('dt', ['type' => 'date']) + ->field('dttm', ['type' => 'datetime']) + ->field('dbl', ['type' => 'double']) + ->field('fl', ['type' => 'float']) + ->field('mn', ['type' => 'money']) + ->field('en', ['type' => 'enum']) ->create(); $m = $this->getMigration(); $m->table('user') - ->newField('zed', ['type'=>'integer']) + ->newField('zed', ['type' => 'integer']) ->alter(); } @@ -38,13 +46,21 @@ public function testCreateAndDrop() $m = $this->getMigration(); $m->table('user')->id() ->field('foo') - ->field('bar', ['type'=>'integer']) - ->field('baz', ['type'=>'text']) + ->field('bar', ['type' => 'integer']) + ->field('baz', ['type' => 'text']) + ->field('bl', ['type' => 'boolean']) + ->field('tm', ['type' => 'time']) + ->field('dt', ['type' => 'date']) + ->field('dttm', ['type' => 'datetime']) + ->field('dbl', ['type' => 'double']) + ->field('fl', ['type' => 'float']) + ->field('mn', ['type' => 'money']) + ->field('en', ['type' => 'enum']) ->create(); $m = $this->getMigration(); $m->table('user') - ->dropField('bar', ['type'=>'integer']) + ->dropField('bar', ['type' => 'integer']) ->alter(); } } diff --git a/tests/ModelTest.php b/tests/ModelTest.php index 60e9a1c..b9bc13f 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -2,14 +2,12 @@ namespace atk4\schema\tests; -use \atk4\schema\Migration\SQLite as Migration; - class ModelTest extends \atk4\schema\PHPUnit_SchemaTestCase { public function testSetModelCreate() { $this->dropTable('user'); - $user = new Testuser($this->db); + $user = new TestUser($this->db); $migration = $this->getMigration($user); $migration->create(); @@ -25,26 +23,53 @@ public function testImportTable() $m = $this->getMigration(); $m->table('user')->id() ->field('foo') - ->field('bar', ['type'=>'integer']) - ->field('baz', ['type'=>'text']) + ->field('str', ['type'=>'string']) + ->field('bool', ['type'=>'boolean']) + ->field('int', ['type'=>'integer']) + ->field('mon', ['type'=>'money']) + ->field('flt', ['type'=>'float']) + ->field('date', ['type'=>'date']) + ->field('datetime', ['type'=>'datetime']) + ->field('time', ['type'=>'time']) + ->field('txt', ['type'=>'text']) + ->field('arr', ['type'=>'array']) + ->field('obj', ['type'=>'object']) ->create(); $this->db->dsql()->table('user') ->set([ - 'id' => 1, - 'foo' => 'foovalue', - 'bar' => 123, - 'baz' => 'long text value', + 'id' => 1, + 'foo' => 'quite short value, max 255 characters', + 'str' => 'quite short value, max 255 characters', + 'bool' => true, + 'int' => 123, + 'mon' => 123.45, + 'flt' => 123.456789, + 'date' => (new \DateTime())->format('Y-m-d'), + 'datetime' => (new \DateTime())->format('Y-m-d H:i:s'), + 'time' => (new \DateTime())->format('H:i:s'), + 'txt' => 'very long text value'.str_repeat('-=#', 1000), // 3000+ chars + 'arr' => 'very long text value'.str_repeat('-=#', 1000), // 3000+ chars + 'obj' => 'very long text value'.str_repeat('-=#', 1000), // 3000+ chars ])->insert(); $m2 = $this->getMigration(); $m2->importTable('user'); $m2->mode('create'); - $this->assertEquals($m->getDebugQuery(), $m2->getDebugQuery()); + + $q1 = preg_replace('/\([0-9,]*\)/i', '', $m->getDebugQuery()); // remove parenthesis otherwise we can't differ money from float etc. + $q2 = preg_replace('/\([0-9,]*\)/i', '', $m2->getDebugQuery()); + $this->assertEquals($q1, $q2); } public function testMigrateTable() { + if ($this->driver == 'sqlite') { + // SQLite doesn't support DROP COLUMN in ALTER TABLE + // http://www.sqlitetutorial.net/sqlite-alter-table/ + $this->markTestIncomplete('This test is not supported on '.$this->driver); + } + $this->dropTable('user'); $m = $this->getMigration($this->db); $m->table('user')->id() @@ -54,7 +79,7 @@ public function testMigrateTable() ->create(); $this->db->dsql()->table('user') ->set([ - 'id' => 1, + 'id' => 1, 'foo' => 'foovalue', 'bar' => 123, 'baz' => 'long text value', @@ -67,12 +92,32 @@ public function testMigrateTable() ->field('baz') ->migrate(); } + + public function testCreateModel() + { + $this->dropTable('user'); + (\atk4\schema\Migration::getMigration(new TestUser($this->db)))->migrate(); + + $m = $this->getMigration($this->db); + $user_model = $m->createModel($this->db, 'user'); + + $this->assertEquals([ + 'name', + 'password', + 'is_admin', + 'notes', + ], + array_keys($user_model->getFields()) + ); + } } -class TestUser extends \atk4\data\Model { +class TestUser extends \atk4\data\Model +{ public $table = 'user'; - public function init() { + public function init() + { parent::init(); $this->addField('name'); @@ -80,5 +125,4 @@ public function init() { $this->addField('is_admin', ['type'=>'boolean']); $this->addField('notes', ['type'=>'text']); } - } diff --git a/tests/SchemaTestcaseTest.php b/tests/SchemaTestcaseTest.php index b355ad5..3791116 100644 --- a/tests/SchemaTestcaseTest.php +++ b/tests/SchemaTestcaseTest.php @@ -2,14 +2,18 @@ namespace atk4\schema\tests; -class SchemaTestcaseTest extends \atk4\schema\PHPUnit_SchemaTestCase +use atk4\schema\PHPUnit_SchemaTestCase; + +class SchemaTestcaseTest extends PHPUnit_SchemaTestCase { public function testInit() { - $this->setDB($q = ['user' => [ - ['name' => 'John', 'surname' => 'Smith'], - ['name' => 'Steve', 'surname' => 'Jobs'], - ]]); + $this->setDB($q = [ + 'user' => [ + ['name' => 'John', 'surname' => 'Smith'], + ['name' => 'Steve', 'surname' => 'Jobs'], + ], + ]); $q2 = $this->getDB('user'); diff --git a/tools/release.sh b/tools/release.sh index 5cc1db9..abfe545 100755 --- a/tools/release.sh +++ b/tools/release.sh @@ -44,20 +44,15 @@ git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen( git log --pretty=full $prev_version... | grep '#[0-9]*' | sed 's/#\([0-9]*\)/\1/' | while read i; do echo '---------------------------------------------------------------------------------' - ghi --color show $i | head -50 done open "https://github.com/atk4/$product/compare/$prev_version...develop" composer remove --dev atk4/data +composer remove --dev atk4/ui composer remove atk4/dsql -composer remove atk4/core -# Tweak our json file -#sed -i "" -e '/atk4.*dev-develop/d' composer.json -#rm -rf vendor/atk4/ -#composer update composer require atk4/dsql -composer require --dev atk4/data +composer require --dev atk4/data atk4/ui composer update ./vendor/phpunit/phpunit/phpunit --no-coverage