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