From 2d39d8f9e4a8a043f052437883298cc885143bc0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2019 03:10:05 +0000 Subject: [PATCH] Releasing refs/heads/release/2.0.0 into master (#18) * add migrator console * Add but more documentation * fix up readme * schema should not depend on development packages * develop to require develop versions * fix alter field * add drop field * add comment * add changelog * fix version requirement * Update composer.json * Update composer.json * Update composer.json * Update BasicTest.php * Update SchemaTestcaseTest.php * Update phpunit.xml * Update .travis.yml * Update phpunit.xml * Update SchemaTestcaseTest.php * Update phpunit.xml * Update SchemaTestcaseTest.php * Update .travis.yml * Update .travis.yml * Update .travis.yml * Update SchemaTestcaseTest.php * move abstract testcase to tests * Apply fixes from StyleCI * enable debug * Apply fixes from StyleCI * move * fix dsn * proper dsn * Update README.md * globals * Apply fixes from StyleCI * work on dsn * Apply fixes from StyleCI * ouch * include both tests - sqlite and mysql * fix * fix types * disable unsupported test * add comment * remove debug * fix codeclimate config * fix escape_char * debug * Apply fixes from StyleCI * crap * PostGre suppport * set max_connections * support PostGreSQL starting from v.10 only * get rid of connection * Update .codeclimate.yml * add support for changing field name * Apply fixes from StyleCI * Update Migration.php * add rename table support * Extend to all DataType options with PHPUnit Tests * add transcode table for field type => datatype database * tested on SQLite and MySQL * Change default Type from VARCHAR256 to TEXT more space is better than less, this class is very useful during development, after that will be disabled and database must be optimized with other tools. i changed because i had a problem storing serialized EXIF in array datatype * drop php 5.6 support * Add transcoding for Field Reference_One climate error correction * Reformatting and change variables name switch to codeformatting PSR-1,PSR-2 add hasOne in Test * add creation of file models class via console to "reverse engineering" DB fixed mysql float was mistyped uppercase * Add Doc for function createModelFromTable * Adjust naming for transcodes on SQLite * add hasOne detect field type * Remove some editor autoadd - f... phpstorm * format code in ->getTranscodeTypeKeyFromField * compatibility with new data namespace * Apply fixes from StyleCI * better handling of text, array and object fields. also fix few others. * Apply fixes from StyleCI * add type options * added changelog * working on version dependencies * for development branch we need development dependencies * make types easier to extend and improve PgSQL types support * Apply fixes from StyleCI * more dependencies and easier to extend migrator console * oops * no need for this anymore * implements factory method getMigration, uses connection->driver, few changes in phpunit test suite, more tests * Apply fixes from StyleCI * change docs * Better PostgreSQL support * fix datetime mess * typo * use getFields() in Model rather than hack through elements * Composer - Drop PHP < 7.2 * Big refactor * Merge remote-tracking branch 'remotes/atk4/develop' into add-type-transcoding * StyleCi * StyleCi 1 * remove php < 7.2 from travis * Removed function for creation of Model File * Refactor variable names and removed extra line * Refactor 1 comment to be consistent with the others below * Add support for GitHub actions (#17) * Add GitHub Action support * disable travis * tweak bundler * wip * wip * wip * Setting current dependencies --- .codeclimate.yml | 93 +++--- .github/release-drafter.yml | 9 + .github/workflows/bundler.yml | 45 +++ .github/workflows/release-drafter.yml | 16 + .github/workflows/unit-tests.yml | 59 ++++ .gitignore | 1 + .old.travis.yml | 51 +++ .travis.yml | 24 -- CHANGELOG.md | 69 +++- README.md | 83 +++-- composer.json | 81 +++-- demos/init.php | 1 + demos/modelmigrator.php | 13 +- docs/migrator-console.png | Bin 0 -> 52541 bytes phpunit-mysql.xml | 24 ++ phpunit.xml | 10 +- src/Migration.php | 462 +++++++++++++++++++------- src/Migration/MySQL.php | 27 +- src/Migration/Oracle.php | 20 ++ src/Migration/PgSQL.php | 90 +++++ src/Migration/SQLite.php | 7 +- src/MigratorConsole.php | 41 +++ src/PHPUnit_SchemaTestCase.php | 48 ++- tests/BasicTest.php | 34 +- tests/ModelTest.php | 72 +++- tests/SchemaTestcaseTest.php | 14 +- tools/release.sh | 9 +- 27 files changed, 1063 insertions(+), 340 deletions(-) create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/bundler.yml create mode 100644 .github/workflows/release-drafter.yml create mode 100644 .github/workflows/unit-tests.yml create mode 100644 .old.travis.yml delete mode 100644 .travis.yml create mode 100644 docs/migrator-console.png create mode 100644 phpunit-mysql.xml create mode 100644 src/Migration/Oracle.php create mode 100644 src/Migration/PgSQL.php create mode 100644 src/MigratorConsole.php 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 0000000000000000000000000000000000000000..d40a9bc01b37493b853ab1ba9cbef2a5e4763e9d GIT binary patch literal 52541 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 literal 0 HcmV?d00001 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