diff --git a/.travis.yml b/.travis.yml index 883da78..49767e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,23 +1,117 @@ language: php +sudo: false +dist: trusty -php: - - 7.2 +cache: + directories: + - vendor + - $HOME/.composer/cache -services: - - postgresql - -matrix: - fast_finish: true +before_install: + - phpenv config-rm xdebug.ini || true + - | + if [ "x$COVERAGE" == "xyes" ]; then + pecl install pcov-1.0.0 + fi install: - - composer install - -before_script: - - psql -c 'create database testing;' -U postgres + - rm composer.lock + - travis_retry composer -n update --prefer-dist script: - vendor/bin/ecs check --config=ecs.yml . - - phpdbg -qrr vendor/bin/phpunit --coverage-clover build/logs/clover.xml + - | + if [ "x$DOCKER_POSTGRES" == "xyes" ]; then + sudo docker exec -ti postgres11 psql postgres -U postgres -c "CREATE DATABASE testing" + else + psql -c 'CREATE DATABASE testing;' -U postgres + fi + - | + if [ "x$COVERAGE" == "xyes" ]; then + ./vendor/bin/phpunit --configuration phpunit.travis.xml --coverage-clover build/logs/clover.xml + else + ./vendor/bin/phpunit --configuration phpunit.travis.xml + fi after_success: - - travis_retry vendor/bin/php-coveralls -v + - | + if [ "x$COVERAGE" == "xyes" ]; then + travis_retry vendor/bin/php-coveralls -v + fi + +matrix: + fast_finish: true + allow_failures: + - php: "7.4snapshot" + include: + - stage: Test + php: "7.2" + env: DB=pgsql POSTGRESQL_VERSION=11.0 + sudo: required + services: + - docker + - stage: Test + php: "7.3" + env: DB=pgsql POSTGRESQL_VERSION=9.2 COVERAGE=yes + services: + - postgresql + addons: + postgresql: "9.2" + - stage: Test + php: "7.3" + env: DB=pgsql POSTGRESQL_VERSION=9.3 COVERAGE=yes + services: + - postgresql + addons: + postgresql: "9.3" + - stage: Test + php: "7.3" + env: DB=pgsql POSTGRESQL_VERSION=9.4 COVERAGE=yes + services: + - postgresql + addons: + postgresql: "9.4" + - stage: Test + php: "7.3" + env: DB=pgsql POSTGRESQL_VERSION=9.5 COVERAGE=yes + services: + - postgresql + addons: + postgresql: "9.5" + - stage: Test + php: "7.3" + env: DB=pgsql POSTGRESQL_VERSION=9.6 COVERAGE=yes + services: + - postgresql + addons: + postgresql: "9.6" + - stage: Test + php: "7.3" + env: DB=pgsql POSTGRESQL_VERSION=10.0 COVERAGE=yes + sudo: required + services: + - postgresql + addons: + postgresql: "9.6" + before_script: + - bash ./tests/travis/install-postgres-10.sh + - stage: Test + php: "7.3" + env: DB=pgsql DOCKER_POSTGRES=yes POSTGRESQL_VERSION=11.0 COVERAGE=yes + sudo: required + services: + - docker + - postgresql + addons: + postgresql: "9.6" + before_script: + - bash ./tests/travis/install-postgres-11.sh + - stage: Test + php: "7.4snapshot" + env: DB=pgsql DOCKER_POSTGRES=yes POSTGRESQL_VERSION=11.0 + sudo: required + services: + - docker + - postgresql + before_script: + - bash ./tests/travis/install-postgres-11.sh diff --git a/README.md b/README.md index 0712d0f..b33eb8b 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ php composer.phar require umbrellio/laravel-pg-extensions - [Extended `Schema::create()`](#extended-table-creation) - [Extended `Schema` with GIST/GIN indexes](#create-gist/gin-indexes) + - [Extended `Schema` with USING](#extended-schema-using) - [Working with unique indexes](#extended-unique-indexes-creation) - [Working with partitions](#partitions) - [Check existing index before manipulation](#check-existing-index) @@ -30,6 +31,24 @@ Schema::create('table', function (Blueprint $table) { }); ``` +### Extended Schema USING + +Example: +```php +Schema::create('table', function (Blueprint $table) { + $table->integer('number'); +}); + +//modifications with data... + +Schema::table('table', function (Blueprint $table) { + $table + ->string('number') + ->using("('[' || number || ']')::character varyiing") + ->change(); +}); +``` + ### Create gist/gin indexes ```php diff --git a/composer.json b/composer.json index dce9b12..49ee59b 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { "name": "umbrellio/laravel-pg-extensions", + "description": "", "type": "library", "minimum-stability": "stable", "authors": [ @@ -10,6 +11,7 @@ ], "require": { "php": "^7.2", + "doctrine/dbal": "^2.9", "laravel/framework": "^5.8" }, "require-dev": { diff --git a/composer.lock b/composer.lock index f8b1d94..4d6cc35 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "806be43cfecda6994cd0335061c2ad39", + "content-hash": "a9b56c70136605e96d824acfe6f3ab54", "packages": [ { "name": "doctrine/cache", @@ -83,31 +83,31 @@ }, { "name": "doctrine/dbal", - "version": "v2.9.2", + "version": "v2.10.4", "source": { "type": "git", - "url": "https://github.com/doctrine/dbal.git", - "reference": "22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9" + "url": "https://github.com/pvsaintpe/dbal.git", + "reference": "59aedf521cca50af294bdbbc14337ea3a0293d86" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9", - "reference": "22800bd651c1d8d2a9719e2a3dc46d5108ebfcc9", + "url": "https://api.github.com/repos/pvsaintpe/dbal/zipball/59aedf521cca50af294bdbbc14337ea3a0293d86", + "reference": "59aedf521cca50af294bdbbc14337ea3a0293d86", "shasum": "" }, "require": { "doctrine/cache": "^1.0", "doctrine/event-manager": "^1.0", "ext-pdo": "*", - "php": "^7.1" + "php": "^7.2" }, "require-dev": { - "doctrine/coding-standard": "^5.0", - "jetbrains/phpstorm-stubs": "^2018.1.2", - "phpstan/phpstan": "^0.10.1", - "phpunit/phpunit": "^7.4", - "symfony/console": "^2.0.5|^3.0|^4.0", - "symfony/phpunit-bridge": "^3.4.5|^4.0.5" + "doctrine/coding-standard": "^6.0", + "jetbrains/phpstorm-stubs": "^2019.1", + "phpstan/phpstan": "^0.11.3", + "phpunit/phpunit": "^8.2.1", + "symfony/console": "^2.0.5|^3.0|^4.0|^5.0", + "symfony/phpunit-bridge": "^3.4.5|^4.0.5|^5.0" }, "suggest": { "symfony/console": "For helpful console commands such as SQL execution and import of files." @@ -118,7 +118,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.9.x-dev", + "dev-master": "2.10.x-dev", "dev-develop": "3.0.x-dev" } }, @@ -127,11 +127,19 @@ "Doctrine\\DBAL\\": "lib/Doctrine/DBAL" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "Doctrine\\Tests\\": "tests/Doctrine/Tests" + } + }, "license": [ "MIT" ], "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, { "name": "Roman Borschel", "email": "roman@code-factory.org" @@ -140,10 +148,6 @@ "name": "Benjamin Eberlei", "email": "kontakt@beberlei.de" }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com" - }, { "name": "Jonathan Wage", "email": "jonwage@gmail.com" @@ -154,14 +158,28 @@ "keywords": [ "abstraction", "database", + "db2", "dbal", + "mariadb", + "mssql", "mysql", - "persistence", + "oci8", + "oracle", + "pdo", "pgsql", - "php", - "queryobject" - ], - "time": "2018-12-31T03:27:51+00:00" + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlanywhere", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "source": "https://github.com/pvsaintpe/dbal/tree/bug/default-expression" + }, + "time": "2019-07-11T16:39:36+00:00" }, { "name": "doctrine/event-manager", @@ -523,16 +541,16 @@ }, { "name": "laravel/framework", - "version": "v5.8.27", + "version": "v5.8.29", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "f1dccffb96f614895393e27e4667105a05407af5" + "reference": "489ae2218c7eb138caac780de584d8df9fe8160b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/f1dccffb96f614895393e27e4667105a05407af5", - "reference": "f1dccffb96f614895393e27e4667105a05407af5", + "url": "https://api.github.com/repos/laravel/framework/zipball/489ae2218c7eb138caac780de584d8df9fe8160b", + "reference": "489ae2218c7eb138caac780de584d8df9fe8160b", "shasum": "" }, "require": { @@ -666,7 +684,7 @@ "framework", "laravel" ], - "time": "2019-07-02T13:43:47+00:00" + "time": "2019-07-16T14:05:28+00:00" }, { "name": "league/flysystem", @@ -832,16 +850,16 @@ }, { "name": "nesbot/carbon", - "version": "2.20.0", + "version": "2.21.3", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "bc671b896c276795fad8426b0aa24e8ade0f2498" + "reference": "58bdbbfab17ccd2ec7347b99e997f18232def4dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/bc671b896c276795fad8426b0aa24e8ade0f2498", - "reference": "bc671b896c276795fad8426b0aa24e8ade0f2498", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/58bdbbfab17ccd2ec7347b99e997f18232def4dc", + "reference": "58bdbbfab17ccd2ec7347b99e997f18232def4dc", "shasum": "" }, "require": { @@ -857,6 +875,9 @@ "phpunit/phpunit": "^7.5 || ^8.0", "squizlabs/php_codesniffer": "^3.4" }, + "bin": [ + "bin/carbon" + ], "type": "library", "extra": { "laravel": { @@ -879,6 +900,10 @@ "name": "Brian Nesbitt", "email": "brian@nesbot.com", "homepage": "http://nesbot.com" + }, + { + "name": "kylekatarnls", + "homepage": "http://github.com/kylekatarnls" } ], "description": "A simple API extension for DateTime.", @@ -888,20 +913,20 @@ "datetime", "time" ], - "time": "2019-06-25T10:00:57+00:00" + "time": "2019-07-18T18:47:28+00:00" }, { "name": "opis/closure", - "version": "3.3.0", + "version": "3.3.1", "source": { "type": "git", "url": "https://github.com/opis/closure.git", - "reference": "f846725591203098246276b2e7b9e8b7814c4965" + "reference": "92927e26d7fc3f271efe1f55bdbb073fbb2f0722" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/opis/closure/zipball/f846725591203098246276b2e7b9e8b7814c4965", - "reference": "f846725591203098246276b2e7b9e8b7814c4965", + "url": "https://api.github.com/repos/opis/closure/zipball/92927e26d7fc3f271efe1f55bdbb073fbb2f0722", + "reference": "92927e26d7fc3f271efe1f55bdbb073fbb2f0722", "shasum": "" }, "require": { @@ -949,7 +974,7 @@ "serialization", "serialize" ], - "time": "2019-05-31T20:04:32+00:00" + "time": "2019-07-09T21:58:11+00:00" }, { "name": "paragonie/random_compat", @@ -3723,34 +3748,35 @@ }, { "name": "ocramius/package-versions", - "version": "1.4.0", + "version": "1.5.1", "source": { "type": "git", "url": "https://github.com/Ocramius/PackageVersions.git", - "reference": "a4d4b60d0e60da2487bd21a2c6ac089f85570dbb" + "reference": "1d32342b8c1eb27353c8887c366147b4c2da673c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/a4d4b60d0e60da2487bd21a2c6ac089f85570dbb", - "reference": "a4d4b60d0e60da2487bd21a2c6ac089f85570dbb", + "url": "https://api.github.com/repos/Ocramius/PackageVersions/zipball/1d32342b8c1eb27353c8887c366147b4c2da673c", + "reference": "1d32342b8c1eb27353c8887c366147b4c2da673c", "shasum": "" }, "require": { "composer-plugin-api": "^1.0.0", - "php": "^7.1.0" + "php": "^7.3.0" }, "require-dev": { - "composer/composer": "^1.6.3", - "doctrine/coding-standard": "^5.0.1", + "composer/composer": "^1.8.6", + "doctrine/coding-standard": "^6.0.0", "ext-zip": "*", - "infection/infection": "^0.7.1", - "phpunit/phpunit": "^7.0.0" + "infection/infection": "^0.13.4", + "phpunit/phpunit": "^8.2.5", + "vimeo/psalm": "^3.4.9" }, "type": "composer-plugin", "extra": { "class": "PackageVersions\\Installer", "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.6.x-dev" } }, "autoload": { @@ -3769,7 +3795,7 @@ } ], "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", - "time": "2019-02-21T12:16:21+00:00" + "time": "2019-07-17T15:49:50+00:00" }, { "name": "orchestra/testbench", @@ -4386,16 +4412,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "7.0.5", + "version": "7.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "aed67b57d459dcab93e84a5c9703d3deb5025dff" + "reference": "d471d0d2b529a67c6a722dd446c4ec90881ac315" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aed67b57d459dcab93e84a5c9703d3deb5025dff", - "reference": "aed67b57d459dcab93e84a5c9703d3deb5025dff", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d471d0d2b529a67c6a722dd446c4ec90881ac315", + "reference": "d471d0d2b529a67c6a722dd446c4ec90881ac315", "shasum": "" }, "require": { @@ -4404,17 +4430,17 @@ "php": "^7.2", "phpunit/php-file-iterator": "^2.0.2", "phpunit/php-text-template": "^1.2.1", - "phpunit/php-token-stream": "^3.0.1", + "phpunit/php-token-stream": "^3.0.2", "sebastian/code-unit-reverse-lookup": "^1.0.1", - "sebastian/environment": "^4.1", + "sebastian/environment": "^4.2.2", "sebastian/version": "^2.0.1", - "theseer/tokenizer": "^1.1" + "theseer/tokenizer": "^1.1.3" }, "require-dev": { - "phpunit/phpunit": "^8.0" + "phpunit/phpunit": "^8.2.2" }, "suggest": { - "ext-xdebug": "^2.6.1" + "ext-xdebug": "^2.7.2" }, "type": "library", "extra": { @@ -4445,7 +4471,7 @@ "testing", "xunit" ], - "time": "2019-06-06T12:28:18+00:00" + "time": "2019-07-08T05:29:42+00:00" }, { "name": "phpunit/php-file-iterator", @@ -4589,16 +4615,16 @@ }, { "name": "phpunit/php-token-stream", - "version": "3.0.1", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18" + "reference": "c4a66b97f040e3e20b3aa2a243230a1c3a9f7c8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c99e3be9d3e85f60646f152f9002d46ed7770d18", - "reference": "c99e3be9d3e85f60646f152f9002d46ed7770d18", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c4a66b97f040e3e20b3aa2a243230a1c3a9f7c8c", + "reference": "c4a66b97f040e3e20b3aa2a243230a1c3a9f7c8c", "shasum": "" }, "require": { @@ -4634,20 +4660,20 @@ "keywords": [ "tokenizer" ], - "time": "2018-10-30T05:52:18+00:00" + "time": "2019-07-08T05:24:54+00:00" }, { "name": "phpunit/phpunit", - "version": "8.2.3", + "version": "8.2.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f67ca36860ebca7224d4573f107f79bd8ed0ba03" + "reference": "c1b8534b3730f20f58600124129197bf1183dc92" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f67ca36860ebca7224d4573f107f79bd8ed0ba03", - "reference": "f67ca36860ebca7224d4573f107f79bd8ed0ba03", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/c1b8534b3730f20f58600124129197bf1183dc92", + "reference": "c1b8534b3730f20f58600124129197bf1183dc92", "shasum": "" }, "require": { @@ -4674,7 +4700,7 @@ "sebastian/global-state": "^3.0.0", "sebastian/object-enumerator": "^3.0.3", "sebastian/resource-operations": "^2.0.1", - "sebastian/type": "^1.1.0", + "sebastian/type": "^1.1.3", "sebastian/version": "^2.0.1" }, "require-dev": { @@ -4717,7 +4743,7 @@ "testing", "xunit" ], - "time": "2019-06-19T12:03:56+00:00" + "time": "2019-07-15T06:26:24+00:00" }, { "name": "psr/cache", @@ -6550,7 +6576,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.1.3" + "php": "^7.2" }, "platform-dev": [] } diff --git a/ecs.yml b/ecs.yml index 768e555..5028a93 100644 --- a/ecs.yml +++ b/ecs.yml @@ -14,3 +14,5 @@ parameters: skip: Symplify\CodingStandard\Sniffs\CleanCode\CognitiveComplexitySniff: - src/Schema/Blueprint.php + Symplify\CodingStandard\Sniffs\CleanCode\ForbiddenReferenceSniff: + - src/Schema/Traits/AlterTableChangeColumnTrait.php diff --git a/phpunit.travis.xml b/phpunit.travis.xml new file mode 100644 index 0000000..22ab00f --- /dev/null +++ b/phpunit.travis.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + ./tests + + + + + ./src + + ./src/.meta.php + + + + diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1dc3aef..1e2c087 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,19 +1,24 @@ - + stopOnFailure="true"> + + + + + + + + ./src ./src/.meta.php - ./src/Commands diff --git a/src/.meta.php b/src/.meta.php index 1fdb06a..9f6ba6d 100644 --- a/src/.meta.php +++ b/src/.meta.php @@ -15,8 +15,19 @@ * @method UniqueDefinition uniquePartial($columns, ?string $index = null, ?string $algorithm = null) * @method Fluent gin($columns, ?string $name = null) * @method Fluent gist($columns, ?string $name = null) + * @method ColumnDefinition tsRange(string $name) + * @method ColumnDefinition tsVector(string $name) */ class Blueprint { } + + /** + * @method ColumnDefinition using($expression) + * @method ColumnDefinition gist() + * @method ColumnDefinition gin() + */ + class ColumnDefinition + { + } } diff --git a/src/Doctrine/RangeExtension.php b/src/Doctrine/RangeExtension.php new file mode 100644 index 0000000..9fc6c45 --- /dev/null +++ b/src/Doctrine/RangeExtension.php @@ -0,0 +1,37 @@ + RangeBlueprint::class, + PostgresGrammar::class => RangeGrammar::class, + ]; + } + + public static function getName(): string + { + return static::NAME; + } + + public static function getTypes(): array + { + return array_merge(parent::getTypes(), [ + TsRangeType::TYPE_NAME => TsRangeType::class, + ]); + } +} diff --git a/src/Doctrine/Schema/Grammars/RangeGrammar.php b/src/Doctrine/Schema/Grammars/RangeGrammar.php new file mode 100644 index 0000000..b43ceab --- /dev/null +++ b/src/Doctrine/Schema/Grammars/RangeGrammar.php @@ -0,0 +1,18 @@ +addColumn(TsRangeType::TYPE_NAME, $column); + }; + } +} diff --git a/src/Doctrine/Schema/VectorBlueprint.php b/src/Doctrine/Schema/VectorBlueprint.php new file mode 100644 index 0000000..f3477a4 --- /dev/null +++ b/src/Doctrine/Schema/VectorBlueprint.php @@ -0,0 +1,19 @@ +addColumn(TsVectorType::TYPE_NAME, $column); + }; + } +} diff --git a/src/Doctrine/Types/TsRangeType.php b/src/Doctrine/Types/TsRangeType.php new file mode 100644 index 0000000..d572a45 --- /dev/null +++ b/src/Doctrine/Types/TsRangeType.php @@ -0,0 +1,41 @@ + VectorBlueprint::class, + PostgresGrammar::class => VectorGrammar::class, + ]; + } + + public static function getName(): string + { + return static::NAME; + } + + public static function getTypes(): array + { + return array_merge(parent::getTypes(), [ + TsVectorType::TYPE_NAME => TsVectorType::class, + ]); + } +} diff --git a/src/Extensions/AbstractComponent.php b/src/Extensions/AbstractComponent.php index 3eb63a7..b03f5a4 100644 --- a/src/Extensions/AbstractComponent.php +++ b/src/Extensions/AbstractComponent.php @@ -4,9 +4,6 @@ namespace Umbrellio\Postgres\Extensions; -/** - * @codeCoverageIgnore - */ abstract class AbstractComponent { final public function __construct() diff --git a/src/Extensions/AbstractExtension.php b/src/Extensions/AbstractExtension.php index 04acb44..b66b9d1 100644 --- a/src/Extensions/AbstractExtension.php +++ b/src/Extensions/AbstractExtension.php @@ -8,9 +8,6 @@ use Umbrellio\Postgres\Extensions\Exceptions\MacroableMissedException; use Umbrellio\Postgres\Extensions\Exceptions\MixinInvalidException; -/** - * @codeCoverageIgnore - */ abstract class AbstractExtension extends AbstractComponent { abstract public static function getMixins(): array; diff --git a/src/PostgresConnection.php b/src/PostgresConnection.php index d1818d1..eddfacf 100644 --- a/src/PostgresConnection.php +++ b/src/PostgresConnection.php @@ -4,12 +4,15 @@ namespace Umbrellio\Postgres; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Events; use Illuminate\Database\PostgresConnection as BasePostgresConnection; use Illuminate\Support\Traits\Macroable; use Umbrellio\Postgres\Extensions\AbstractExtension; use Umbrellio\Postgres\Extensions\Exceptions\ExtensionInvalidException; use Umbrellio\Postgres\Schema\Builder; use Umbrellio\Postgres\Schema\Grammars\PostgresGrammar; +use Umbrellio\Postgres\Schema\Listeners\SchemaAlterTableChangeColumnListener; class PostgresConnection extends BasePostgresConnection { @@ -19,6 +22,7 @@ class PostgresConnection extends BasePostgresConnection /** * @param AbstractExtension|string $extension + * @throws ExtensionInvalidException * @codeCoverageIgnore */ final public static function registerExtension(string $extension): void @@ -41,23 +45,27 @@ public function getSchemaBuilder() return new Builder($this); } - public function useDefaultPostProcessor() + public function useDefaultPostProcessor(): void { parent::useDefaultPostProcessor(); $this->registerExtensions(); } + public function getDoctrineConnection(): Connection + { + $doctrineConnection = parent::getDoctrineConnection(); + $this->overrideDoctrineBehavior($doctrineConnection); + return $doctrineConnection; + } + protected function getDefaultSchemaGrammar() { return $this->withTablePrefix(new PostgresGrammar()); } - /** - * @codeCoverageIgnore - */ - final private function registerExtensions(): void + private function registerExtensions(): void { - collect(self::$extensions)->each(function ($extension, $key) { + collect(self::$extensions)->each(function ($extension) { /** @var AbstractExtension $extension */ $extension::register(); foreach ($extension::getTypes() as $type => $typeClass) { @@ -65,4 +73,17 @@ final private function registerExtensions(): void } }); } + + private function overrideDoctrineBehavior(Connection $connection): Connection + { + $eventManager = $connection->getEventManager(); + if (!$eventManager->hasListeners(Events::onSchemaAlterTableChangeColumn)) { + $eventManager->addEventListener( + Events::onSchemaAlterTableChangeColumn, + new SchemaAlterTableChangeColumnListener() + ); + } + $connection->getDatabasePlatform()->setEventManager($eventManager); + return $connection; + } } diff --git a/src/Schema/Grammars/PostgresGrammar.php b/src/Schema/Grammars/PostgresGrammar.php index 088a14e..6236d6e 100644 --- a/src/Schema/Grammars/PostgresGrammar.php +++ b/src/Schema/Grammars/PostgresGrammar.php @@ -60,7 +60,6 @@ public function compileGin(Blueprint $blueprint, Fluent $command): string ); } - public function compileGist(Blueprint $blueprint, Fluent $command): string { return sprintf( diff --git a/src/Schema/Listeners/SchemaAlterTableChangeColumnListener.php b/src/Schema/Listeners/SchemaAlterTableChangeColumnListener.php new file mode 100644 index 0000000..d65fed9 --- /dev/null +++ b/src/Schema/Listeners/SchemaAlterTableChangeColumnListener.php @@ -0,0 +1,26 @@ +preventDefault(); + + $event->addSql( + $this->getAlterTableChangeColumnSQL( + $event->getPlatform(), + $event->getTableDiff(), + $event->getColumnDiff() + ) + ); + } +} diff --git a/src/Schema/Traits/AlterTableChangeColumnTrait.php b/src/Schema/Traits/AlterTableChangeColumnTrait.php new file mode 100644 index 0000000..245ec9b --- /dev/null +++ b/src/Schema/Traits/AlterTableChangeColumnTrait.php @@ -0,0 +1,208 @@ +quoteName($platform, $diff); + + $oldColumnName = $columnDiff->getOldColumnName()->getQuotedName($platform); + $column = $columnDiff->column; + + $this->compileAlterColumnType($platform, $columnDiff, $column, $quoteName, $oldColumnName, $sql); + + $this->compileAlterColumnDefault($platform, $columnDiff, $column, $quoteName, $oldColumnName, $sql); + + $this->compileAlterColumnNull($columnDiff, $column, $quoteName, $oldColumnName, $sql); + + $this->compileAlterColumnSequence($platform, $columnDiff, $diff, $column, $quoteName, $oldColumnName, $sql); + + $this->compileAlterColumnComment($platform, $columnDiff, $column, $quoteName, $sql); + + if (!$columnDiff->hasChanged('length')) { + return $sql; + } + + $sql[] = sprintf( + 'ALTER TABLE %s ALTER %s TYPE %s', + $quoteName, + $oldColumnName, + $column->getType()->getSQLDeclaration($column->toArray(), $platform) + ); + + return $sql; + } + + private function compileAlterColumnComment( + AbstractPlatform $platform, + ColumnDiff $columnDiff, + Column $column, + string $quoteName, + &$sql + ): void { + $newComment = $this->getColumnComment($column); + $oldComment = $this->getOldColumnComment($columnDiff); + + if (($columnDiff->fromColumn !== null && $oldComment !== $newComment) + || $columnDiff->hasChanged('comment') + ) { + $sql[] = $platform->getCommentOnColumnSQL($quoteName, $column->getQuotedName($platform), $newComment); + } + } + + private function compileAlterColumnNull( + ColumnDiff $columnDiff, + Column $column, + string $quoteName, + string $oldColumnName, + &$sql + ): void { + if ($columnDiff->hasChanged('notnull')) { + $sql[] = sprintf( + 'ALTER TABLE %s ALTER %s %s NOT NULL', + $quoteName, + $oldColumnName, + ($column->getNotnull() ? 'SET' : 'DROP') + ); + } + } + + private function compileAlterColumnDefault( + AbstractPlatform $platform, + ColumnDiff $columnDiff, + Column $column, + string $quoteName, + string $oldColumnName, + &$sql + ): void { + if ($columnDiff->hasChanged('default') || $this->typeChangeBreaksDefaultValue($columnDiff)) { + $defaultClause = $column->getDefault() === null + ? ' DROP DEFAULT' + : ' SET' . $this->getDefaultValueDeclarationSQL($platform, $column); + $sql[] = sprintf('ALTER TABLE %s ALTER %s %s', $quoteName, $oldColumnName, trim($defaultClause)); + } + } + + private function compileAlterColumnSequence( + AbstractPlatform $platform, + ColumnDiff $columnDiff, + TableDiff $diff, + Column $column, + string $quoteName, + string $oldColumnName, + &$sql + ): void { + if (!$columnDiff->hasChanged('autoincrement')) { + return; + } + + if (!$column->getAutoincrement()) { + $sql[] = sprintf('ALTER TABLE %s ALTER %s DROP DEFAULT', $quoteName, $oldColumnName); + return; + } + + $seqName = $platform->getIdentitySequenceName($diff->name, $oldColumnName); + + $sql[] = sprintf('CREATE SEQUENCE %s', $seqName); + $sql[] = sprintf("SELECT setval('%s', (SELECT MAX(%s) FROM %s))", $seqName, $oldColumnName, $quoteName); + $sql[] = sprintf("ALTER TABLE %s ALTER %s SET DEFAULT nextval('%s')", $quoteName, $oldColumnName, $seqName); + } + + private function compileAlterColumnType( + AbstractPlatform $platform, + ColumnDiff $columnDiff, + Column $column, + string $quoteName, + string $oldColumnName, + &$sql + ): void { + if ($columnDiff->hasChanged('type') + || $columnDiff->hasChanged('precision') + || $columnDiff->hasChanged('scale') + || $columnDiff->hasChanged('fixed') + ) { + $type = $column->getType(); + + $columnDefinition = $column->toArray(); + $columnDefinition['autoincrement'] = false; + + if ($this->typeChangeBreaksDefaultValue($columnDiff)) { + $sql[] = sprintf('ALTER TABLE %s ALTER %s DROP DEFAULT', $quoteName, $oldColumnName); + } + + $typeName = $type->getSQLDeclaration($columnDefinition, $platform); + + if ($columnDiff->hasChanged('type')) { + $using = sprintf('USING %s::%s', $oldColumnName, $typeName); + + if ($columnDefinition['using'] ?? false) { + $using = 'USING ' . $columnDefinition['using']; + } + } + + $sql[] = trim(sprintf( + 'ALTER TABLE %s ALTER %s TYPE %s %s', + $quoteName, + $oldColumnName, + $typeName, + $using ?? '' + )); + } + } + + private function getDefaultValueDeclarationSQL(AbstractPlatform $platform, Column $column): string + { + if ($column->getDefault() instanceof Expression) { + return ' DEFAULT ' . $column->getDefault(); + } + + return $platform->getDefaultValueDeclarationSQL($column->toArray()); + } + + private function typeChangeBreaksDefaultValue(ColumnDiff $columnDiff): bool + { + $oldTypeIsNumeric = $this->isNumericType($columnDiff->fromColumn->getType()); + $newTypeIsNumeric = $this->isNumericType($columnDiff->column->getType()); + + return $columnDiff->hasChanged('type') + && !($oldTypeIsNumeric && $newTypeIsNumeric && $columnDiff->column->getAutoincrement()); + } + + private function isNumericType(Type $type): bool + { + return $type instanceof IntegerType || $type instanceof BigIntType; + } + + private function quoteName(AbstractPlatform $platform, TableDiff $diff): string + { + return $diff->getName($platform)->getQuotedName($platform); + } + + private function getOldColumnComment(ColumnDiff $columnDiff): ?string + { + return $columnDiff->fromColumn ? $this->getColumnComment($columnDiff->fromColumn) : null; + } + + private function getColumnComment(Column $column): ?string + { + return $column->getComment(); + } +} diff --git a/src/UmbrellioPostgresProvider.php b/src/UmbrellioPostgresProvider.php index 2da00c4..1e4f57e 100644 --- a/src/UmbrellioPostgresProvider.php +++ b/src/UmbrellioPostgresProvider.php @@ -7,9 +7,18 @@ use Illuminate\Database\DatabaseManager; use Illuminate\Database\DatabaseServiceProvider; use Umbrellio\Postgres\Connectors\ConnectionFactory; +use Umbrellio\Postgres\Doctrine\RangeExtension; +use Umbrellio\Postgres\Doctrine\VectorExtension; class UmbrellioPostgresProvider extends DatabaseServiceProvider { + public function register() + { + parent::register(); + PostgresConnection::registerExtension(RangeExtension::class); + PostgresConnection::registerExtension(VectorExtension::class); + } + /** * @codeCoverageIgnore */ diff --git a/tests.sh b/tests.sh new file mode 100755 index 0000000..c4ce18c --- /dev/null +++ b/tests.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +psql postgres -U user -tc "SELECT 1 FROM pg_database WHERE datname = 'testing'" | grep -q 1 || psql postgres -U user -c "CREATE DATABASE testing" +composer lint +php -d pcov.directory='.' vendor/bin/phpunit --coverage-html build diff --git a/tests/Functional/FunctionalTestCase.php b/tests/Functional/FunctionalTestCase.php deleted file mode 100644 index af522b6..0000000 --- a/tests/Functional/FunctionalTestCase.php +++ /dev/null @@ -1,31 +0,0 @@ -set('database.default', 'testing'); - $app['config']->set('database.connections.testing', [ - 'driver' => 'pgsql', - 'host' => env('TEST_DB_HOST', 'localhost'), - 'port' => env('TEST_DB_PORT', 5432), - 'database' => env('TEST_DB', 'testing'), - 'username' => env('TEST_DB_USER', 'postgres'), - 'password' => env('TEST_DB_PASSWORD', ''), - 'charset' => 'utf8', - 'prefix' => '', - 'schema' => 'public', - ]); - } -} diff --git a/tests/Functional/HasIndexTest.php b/tests/Functional/HasIndexTest.php deleted file mode 100644 index 811ef37..0000000 --- a/tests/Functional/HasIndexTest.php +++ /dev/null @@ -1,42 +0,0 @@ -increments('id'); - $table->string('name'); - - if (!$table->hasIndex(['name'], true)) { - $table->unique(['name']); - } - }); - - $this->assertTrue(Schema::hasTable('test_table')); - - $indexes = $this->getIndexByName('test_table_name_unique'); - - Schema::table('test_table', function (Blueprint $table) { - if (!$table->hasIndex(['name'], true)) { - $table->unique(['name']); - } - }); - - $this->assertTrue(isset($indexes->indexdef)); - } - - protected function getIndexByName($name) - { - return collect(DB::select("SELECT indexdef FROM pg_indexes WHERE indexname = '{$name}'"))->first(); - } -} diff --git a/tests/Functional/Helpers/ColumnAssertions.php b/tests/Functional/Helpers/ColumnAssertions.php new file mode 100644 index 0000000..dc7e68a --- /dev/null +++ b/tests/Functional/Helpers/ColumnAssertions.php @@ -0,0 +1,72 @@ +getCommentListing($table, $column); + + if ($expected === null) { + $this->assertNull($comment); + } + + $this->assertSame($expected, $comment); + } + + protected function assertDefaultOnColumn(string $table, string $column, ?string $expected = null): void + { + $defaultValue = $this->getDefaultListing($table, $column); + + if ($expected === null) { + $this->assertNull($defaultValue); + } + + $this->assertSame($expected, $defaultValue); + } + + protected function assertTypeColumn(string $table, string $column, string $expected): void + { + $this->assertSame($expected, Schema::getColumnType($table, $column)); + } + private function getCommentListing(string $table, string $column) + { + $definition = DB::selectOne( + ' + SELECT pgd.description + FROM pg_catalog.pg_statio_all_tables AS st + INNER JOIN pg_catalog.pg_description pgd ON (pgd.objoid = st.relid) + INNER JOIN information_schema.columns c ON pgd.objsubid = c.ordinal_position + AND c.table_schema = st.schemaname AND c.table_name = st.relname + WHERE c.table_name = ? AND c.column_name = ? + ', + [$table, $column] + ); + + return $definition ? $definition->description : null; + } + + private function getDefaultListing(string $table, string $column) + { + $definition = DB::selectOne( + ' + SELECT column_default + FROM information_schema.columns c + WHERE c.table_name = ? and c.column_name = ? + ', + [$table, $column] + ); + + return $definition ? $definition->column_default : null; + } +} diff --git a/tests/Functional/Helpers/IndexAssertions.php b/tests/Functional/Helpers/IndexAssertions.php new file mode 100644 index 0000000..07d23d0 --- /dev/null +++ b/tests/Functional/Helpers/IndexAssertions.php @@ -0,0 +1,41 @@ +assertNotNull($this->getIndexListing($index)); + } + + protected function assertSameIndex(string $index, string $expectedDef): void + { + $definition = $this->getIndexListing($index); + + $this->seeIndex($index); + $this->assertSame($expectedDef, $definition); + } + + protected function assertRegExpIndex(string $index, string $expectedDef): void + { + $definition = $this->getIndexListing($index); + + $this->seeIndex($index); + $this->assertRegExp($expectedDef, $definition); + } + private function getIndexListing($index): ?string + { + $definition = DB::selectOne('SELECT indexdef FROM pg_indexes WHERE indexname = ?', [$index]); + + return $definition ? $definition->indexdef : null; + } +} diff --git a/tests/Functional/Helpers/TableAssertions.php b/tests/Functional/Helpers/TableAssertions.php new file mode 100644 index 0000000..c3a2c90 --- /dev/null +++ b/tests/Functional/Helpers/TableAssertions.php @@ -0,0 +1,36 @@ +assertSame($this->getTableDefinition($sourceTable), $this->getTableDefinition($destinationTable)); + } + + protected function assertSameTable(array $expectedDef, string $table): void + { + $definition = $this->getTableDefinition($table); + + $this->assertSame($expectedDef, $definition); + } + + protected function seeTable(string $table): void + { + $this->assertTrue(Schema::hasTable($table)); + } + + private function getTableDefinition(string $table): array + { + return Schema::getColumnListing($table); + } +} diff --git a/tests/Functional/Schema/AddColumnsTest.php b/tests/Functional/Schema/AddColumnsTest.php new file mode 100644 index 0000000..c58b326 --- /dev/null +++ b/tests/Functional/Schema/AddColumnsTest.php @@ -0,0 +1,40 @@ +assertTypeColumn('test_table', 'field_range', $type); + } + + public function provideRangeTypes(): Generator + { + yield ['tsrange', function (Blueprint $table, string $column) { + $table->tsRange($column); + }]; + yield ['text', function (Blueprint $table, string $column) { + $table->tsVector($column); + }]; + } +} diff --git a/tests/Functional/Schema/AlterColumnsTest.php b/tests/Functional/Schema/AlterColumnsTest.php new file mode 100644 index 0000000..dcb7deb --- /dev/null +++ b/tests/Functional/Schema/AlterColumnsTest.php @@ -0,0 +1,246 @@ +string('code')->default('1'); + }); + + $this->assertDefaultOnColumn('test_table', 'code', "'1'::character varying"); + + Schema::table('test_table', function (Blueprint $table) { + $table->string('code')->comment('some comment')->change(); + }); + + $this->assertCommentOnColumn('test_table', 'code', 'some comment'); + $this->assertDefaultOnColumn('test_table', 'code', "'1'::character varying"); + } + + /** @test */ + public function alterTableJsonSetComment(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->string('json_field'); + }); + + $this->assertCommentOnColumn('test_table', 'json_field'); + + Schema::table('test_table', function (Blueprint $table) { + $table->json('json_field')->comment('(DC2Type:json_array)')->change(); + }); + + $this->assertCommentOnColumn('test_table', 'json_field', '(DC2Type:json_array)'); + } + + /** @test */ + public function alterTableSetDCComment(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->string('code')->default('1'); + }); + + $this->assertDefaultOnColumn('test_table', 'code', "'1'::character varying"); + $this->assertCommentOnColumn('test_table', 'code'); + + Schema::table('test_table', function (Blueprint $table) { + $table->string('code')->comment('(DC2Type:string)')->change(); + }); + + $this->assertDefaultOnColumn('test_table', 'code', "'1'::character varying"); + $this->assertCommentOnColumn('test_table', 'code', '(DC2Type:string)'); + } + + /** @test */ + public function alterTableDropDCComment(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->integer('number')->comment('(DC2Type:integer)')->default(1); + }); + + $this->assertCommentOnColumn('test_table', 'number', '(DC2Type:integer)'); + + Schema::table('test_table', function (Blueprint $table) { + $table->integer('number')->comment('test')->change(); + }); + + $this->assertCommentOnColumn('test_table', 'number', 'test'); + } + + /** @test */ + public function alterTableChangeSimpleComment(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->integer('number')->comment('(DC2Type:integer)')->default(1); + }); + + $this->assertDefaultOnColumn('test_table', 'number', '1'); + + Schema::table('test_table', function (Blueprint $table) { + $table->string('number')->comment('some comment')->change(); + }); + + $this->assertCommentOnColumn('test_table', 'number', 'some comment'); + $this->assertDefaultOnColumn('test_table', 'number', "'1'::character varying"); + } + + /** @test */ + public function alterTableUsingByDefault(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->string('code')->default('1'); + }); + + $this->assertDefaultOnColumn('test_table', 'code', "'1'::character varying"); + + Schema::table('test_table', function (Blueprint $table) { + $table->integer('code')->default(null)->change(); + }); + + $this->assertTypeColumn('test_table', 'code', 'integer'); + $this->assertDefaultOnColumn('test_table', 'code'); + } + + /** @test */ + public function alterTableUsingWithExpression(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->integer('id')->primary(); + $table->integer('number')->default('1')->nullable(); + }); + + $this->assertDefaultOnColumn('test_table', 'number', '1'); + + DB::table('test_table')->insert([['id' => 1]]); + + $this->assertDatabaseHas('test_table', ['id' => 1]); + $this->assertTypeColumn('test_table', 'number', 'integer'); + + Schema::table('test_table', function (Blueprint $table) { + $table->string('number') + ->using("('[' || number || ']')::character varying") + ->change(); + }); + + $this->assertDefaultOnColumn('test_table', 'number', "'1'::character varying"); + $this->assertTypeColumn('test_table', 'number', 'string'); + $this->assertDatabaseHas('test_table', [ + 'id' => 1, + 'number' => '[1]', + ]); + } + + /** @test */ + public function alterTableSetDefault(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->integer('code')->nullable(); + }); + + $this->assertTypeColumn('test_table', 'code', 'integer'); + $this->assertDefaultOnColumn('test_table', 'code'); + + Schema::table('test_table', function (Blueprint $table) { + $table->string('code')->default('test_string')->change(); + }); + + $this->assertTypeColumn('test_table', 'code', 'string'); + $this->assertDefaultOnColumn('test_table', 'code', "'test_string'::character varying"); + } + + /** @test */ + public function alterTableChangeDefault(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->string('description')->default('default1'); + }); + + $this->assertDefaultOnColumn('test_table', 'description', "'default1'::character varying"); + + Schema::table('test_table', function (Blueprint $table) { + $table->text('description')->default('default2')->change(); + }); + + $this->assertDefaultOnColumn('test_table', 'description', "'default2'::text"); + } + + /** @test */ + public function alterTableDropDefault(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->string('description')->default('default_value'); + }); + + $this->assertDefaultOnColumn('test_table', 'description', "'default_value'::character varying"); + + Schema::table('test_table', function (Blueprint $table) { + $table->string('description')->nullable()->default(null)->change(); + }); + + $this->assertDefaultOnColumn('test_table', 'description'); + } + + /** @test */ + public function alterTableSetDefaultExpression(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->string('code')->nullable(); + }); + + $this->assertDefaultOnColumn('test_table', 'code'); + + Schema::table('test_table', function (Blueprint $table) { + $table->string('code')->default(new Expression("''::character varying"))->change(); + }); + + $this->assertDefaultOnColumn('test_table', 'code', "''::character varying"); + } + + /** @test */ + public function alterTableCreateSequence(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->integer('id')->default(1); + }); + + $this->assertDefaultOnColumn('test_table', 'id', '1'); + + Schema::table('test_table', function (Blueprint $table) { + $table->increments('id')->change(); + }); + + $this->assertDefaultOnColumn('test_table', 'id', "nextval('test_table_id_seq'::regclass)"); + } + + /** @test */ + public function alterTableDropSequence(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->increments('id'); + }); + + $this->assertDefaultOnColumn('test_table', 'id', "nextval('test_table_id_seq'::regclass)"); + + Schema::table('test_table', function (Blueprint $table) { + $table->integer('id')->change(); + }); + + $this->assertDefaultOnColumn('test_table', 'id'); + } +} diff --git a/tests/Functional/Schema/CreateIndexTest.php b/tests/Functional/Schema/CreateIndexTest.php new file mode 100644 index 0000000..938dff0 --- /dev/null +++ b/tests/Functional/Schema/CreateIndexTest.php @@ -0,0 +1,202 @@ +tsRange('code')->gist(); + }); + + $this->seeIndex('test_table_code_gist'); + + Schema::table('test_table', function (Blueprint $table) { + $table->tsRange('some_id'); + $table->tsRange('some_key'); + $table->gist('some_key', 'specify_gist_key'); + $table->gist('some_id'); + }); + + $this->seeIndex('specify_gist_key'); + $this->seeIndex('test_table_some_id_gist'); + } + + /** @test */ + public function createGinIndex(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->tsVector('id')->gin(); + }); + + $this->seeIndex('test_table_id_gin'); + + Schema::table('test_table', function (Blueprint $table) { + $table->tsVector('some_id'); + $table->tsVector('some_key'); + $table->gin('some_key', 'specify_gin_key'); + $table->gin('some_id'); + }); + + $this->seeIndex('specify_gin_key'); + $this->seeIndex('test_table_some_id_gin'); + } + + /** @test */ + public function createIndexIfNotExists(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + + if (!$table->hasIndex(['name'], true)) { + $table->unique(['name']); + } + }); + + $this->assertTrue(Schema::hasTable('test_table')); + + Schema::table('test_table', function (Blueprint $table) { + if (!$table->hasIndex(['name'], true)) { + $table->unique(['name']); + } + }); + + $this->seeIndex('test_table_name_unique'); + } + + /** + * @test + * @dataProvider provideIndexes + */ + public function createPartialUniqueWithNull(string $expected, Closure $callback): void + { + Schema::create('test_table', function (Blueprint $table) use ($callback) { + $table->increments('id'); + $table->string('name'); + $table->string('code'); + $table->integer('phone'); + $table->boolean('enabled'); + $table->integer('icq'); + $table->softDeletes(); + + $callback($table); + }); + + $this->assertTrue(Schema::hasTable('test_table')); + $this->assertRegExpIndex('test_table_name_unique', '/' . $this->getDummyIndex() . $expected . '/'); + } + + /** @test */ + public function createSpecifyIndex(): void + { + Schema::create('test_table', function (Blueprint $table) { + $table->string('name')->index('specify_index_name'); + }); + + $this->assertTrue(Schema::hasTable('test_table')); + + $this->assertRegExpIndex( + 'specify_index_name', + '/CREATE INDEX specify_index_name ON (public.)?test_table USING btree \(name\)/' + ); + } + + public function provideIndexes(): Generator + { + yield ['', function (Blueprint $table) { + $table->uniquePartial('name'); + }]; + yield [ + ' WHERE \(deleted_at IS NULL\)', + function (Blueprint $table) { + $table->uniquePartial('name')->whereNull('deleted_at'); + }, + ]; + yield [ + ' WHERE \(deleted_at IS NOT NULL\)', + function (Blueprint $table) { + $table->uniquePartial('name')->whereNotNull('deleted_at'); + }, + ]; + yield [ + ' WHERE \(phone = 1234\)', + function (Blueprint $table) { + $table->uniquePartial('name')->where('phone', '=', 1234); + }, + ]; + yield [ + " WHERE \(\(code\)::text = 'test'::text\)", + function (Blueprint $table) { + $table->uniquePartial('name')->where('code', '=', 'test'); + }, + ]; + yield [ + ' WHERE \(\(phone >= 1\) AND \(phone <= 2\)\)', + function (Blueprint $table) { + $table->uniquePartial('name')->whereBetween('phone', [1, 2]); + }, + ]; + yield [ + ' WHERE \(\(phone < 1\) OR \(phone > 2\)\)', + function (Blueprint $table) { + $table->uniquePartial('name')->whereNotBetween('phone', [1, 2]); + }, + ]; + yield [ + ' WHERE \(phone <> icq\)', + function (Blueprint $table) { + $table->uniquePartial('name')->whereColumn('phone', '<>', 'icq'); + }, + ]; + yield [ + ' WHERE \(\(phone = 1\) AND \(icq < 2\)\)', + function (Blueprint $table) { + $table->uniquePartial('name')->whereRaw('phone = ? and icq < ?', [1, 2]); + }, + ]; + yield [ + ' WHERE \(phone = ANY \(ARRAY\[1, 2, 4\]\)\)', + function (Blueprint $table) { + $table->uniquePartial('name')->whereIn('phone', [1, 2, 4]); + }, + ]; + yield [ + ' WHERE \(0 = 1\)', + function (Blueprint $table) { + $table->uniquePartial('name')->whereIn('phone', []); + }, + ]; + yield [ + ' WHERE \(phone <> ALL \(ARRAY\[1, 2, 4\]\)\)', + function (Blueprint $table) { + $table->uniquePartial('name')->whereNotIn('phone', [1, 2, 4]); + }, + ]; + yield [ + ' WHERE \(1 = 1\)', + function (Blueprint $table) { + $table->uniquePartial('name')->whereNotIn('phone', []); + }, + ]; + } + + protected function getDummyIndex(): string + { + return 'CREATE UNIQUE INDEX test_table_name_unique ON (public.)?test_table USING btree \(name\)'; + } +} diff --git a/tests/Functional/SchemaTest.php b/tests/Functional/Schema/CreateTableTest.php similarity index 53% rename from tests/Functional/SchemaTest.php rename to tests/Functional/Schema/CreateTableTest.php index 3e9f825..ce234a0 100644 --- a/tests/Functional/SchemaTest.php +++ b/tests/Functional/Schema/CreateTableTest.php @@ -2,27 +2,32 @@ declare(strict_types=1); -namespace Umbrellio\Postgres\Tests\Functional; +namespace Umbrellio\Postgres\Tests\Functional\Schema; +use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Support\Facades\Schema; use Umbrellio\Postgres\Schema\Blueprint; +use Umbrellio\Postgres\Tests\Functional\Helpers\TableAssertions; +use Umbrellio\Postgres\Tests\FunctionalTestCase; -class SchemaTest extends FunctionalTestCase +class CreateTableTest extends FunctionalTestCase { + use DatabaseTransactions, TableAssertions; + /** @test */ - public function create(): void + public function createSimple(): void { Schema::create('test_table', function (Blueprint $table) { $table->increments('id'); $table->string('name'); }); - $this->assertTrue(Schema::hasTable('test_table')); - $this->assertSame(['id', 'name'], Schema::getColumnListing('test_table')); + $this->seeTable('test_table'); + $this->assertSameTable(['id', 'name'], 'test_table'); } /** @test */ - public function createLikeSimple(): void + public function createViaLike(): void { Schema::create('test_table', function (Blueprint $table) { $table->increments('id'); @@ -33,14 +38,13 @@ public function createLikeSimple(): void $table->like('test_table'); }); - $this->assertTrue(Schema::hasTable('test_table')); - $this->assertTrue(Schema::hasTable('test_table2')); - - $this->assertSame(Schema::getColumnListing('test_table'), Schema::getColumnListing('test_table2')); + $this->seeTable('test_table'); + $this->seeTable('test_table2'); + $this->assertCompareTables('test_table', 'test_table2'); } /** @test */ - public function createLikeFull(): void + public function createViaLikeIncludingAll(): void { Schema::create('test_table', function (Blueprint $table) { $table->increments('id'); @@ -52,8 +56,8 @@ public function createLikeFull(): void $table->ifNotExists(); }); - $this->assertTrue(Schema::hasTable('test_table')); - $this->assertTrue(Schema::hasTable('test_table2')); - $this->assertSame(Schema::getColumnListing('test_table'), Schema::getColumnListing('test_table2')); + $this->seeTable('test_table'); + $this->seeTable('test_table2'); + $this->assertCompareTables('test_table', 'test_table2'); } } diff --git a/tests/Functional/UniqueIndexTest.php b/tests/Functional/UniqueIndexTest.php deleted file mode 100644 index 8de9265..0000000 --- a/tests/Functional/UniqueIndexTest.php +++ /dev/null @@ -1,142 +0,0 @@ -increments('id'); - $table->string('name'); - $table->string('code'); - $table->integer('phone'); - $table->boolean('enabled'); - $table->integer('icq'); - $table->softDeletes(); - $callback($table); - }); - - $this->assertTrue(Schema::hasTable('test_table')); - - $indexes = $this->getIndexByName('test_table_name_unique'); - - $this->assertTrue(isset($indexes->indexdef)); - $this->assertSame($this->getDummyIndex() . $expected, $indexes->indexdef); - } - - /** @test */ - public function createSpecifyIndex(): void - { - Schema::create('test_table', function (Blueprint $table) { - $table->string('name')->index('specify_index_name'); - }); - - $this->assertTrue(Schema::hasTable('test_table')); - - $this->assertSame( - 'CREATE INDEX specify_index_name ON public.test_table USING btree (name)', - $this->getIndexByName('specify_index_name')->indexdef - ); - } - - public function provideIndexes(): Generator - { - yield ['', function (Blueprint $table) { - $table->uniquePartial('name'); - }]; - yield [ - ' WHERE (deleted_at IS NULL)', - function (Blueprint $table) { - $table->uniquePartial('name')->whereNull('deleted_at'); - }, - ]; - yield [ - ' WHERE (deleted_at IS NOT NULL)', - function (Blueprint $table) { - $table->uniquePartial('name')->whereNotNull('deleted_at'); - }, - ]; - yield [ - ' WHERE (phone = 1234)', - function (Blueprint $table) { - $table->uniquePartial('name')->where('phone', '=', 1234); - }, - ]; - yield [ - " WHERE ((code)::text = 'test'::text)", - function (Blueprint $table) { - $table->uniquePartial('name')->where('code', '=', 'test'); - }, - ]; - yield [ - ' WHERE ((phone >= 1) AND (phone <= 2))', - function (Blueprint $table) { - $table->uniquePartial('name')->whereBetween('phone', [1, 2]); - }, - ]; - yield [ - ' WHERE ((phone < 1) OR (phone > 2))', - function (Blueprint $table) { - $table->uniquePartial('name')->whereNotBetween('phone', [1, 2]); - }, - ]; - yield [ - ' WHERE (phone <> icq)', - function (Blueprint $table) { - $table->uniquePartial('name')->whereColumn('phone', '<>', 'icq'); - }, - ]; - yield [ - ' WHERE ((phone = 1) AND (icq < 2))', - function (Blueprint $table) { - $table->uniquePartial('name')->whereRaw('phone = ? and icq < ?', [1, 2]); - }, - ]; - yield [ - ' WHERE (phone = ANY (ARRAY[1, 2, 4]))', - function (Blueprint $table) { - $table->uniquePartial('name')->whereIn('phone', [1, 2, 4]); - }, - ]; - yield [ - ' WHERE (0 = 1)', - function (Blueprint $table) { - $table->uniquePartial('name')->whereIn('phone', []); - }, - ]; - yield [ - ' WHERE (phone <> ALL (ARRAY[1, 2, 4]))', - function (Blueprint $table) { - $table->uniquePartial('name')->whereNotIn('phone', [1, 2, 4]); - }, - ]; - yield [ - ' WHERE (1 = 1)', - function (Blueprint $table) { - $table->uniquePartial('name')->whereNotIn('phone', []); - }, - ]; - } - - protected function getDummyIndex() - { - return 'CREATE UNIQUE INDEX test_table_name_unique ON public.test_table USING btree (name)'; - } - - protected function getIndexByName($name) - { - return collect(DB::select("SELECT indexdef FROM pg_indexes WHERE indexname = '{$name}'"))->first(); - } -} diff --git a/tests/FunctionalTestCase.php b/tests/FunctionalTestCase.php new file mode 100644 index 0000000..ecf2691 --- /dev/null +++ b/tests/FunctionalTestCase.php @@ -0,0 +1,49 @@ +getConnectionParams(); + + $app['config']->set('database.default', 'main'); + $app['config']->set('database.connections.main', [ + 'driver' => 'pgsql', + 'host' => $params['host'], + 'port' => (int) $params['port'], + 'database' => $params['database'], + 'username' => $params['user'], + 'password' => $params['password'], + 'charset' => 'utf8', + 'prefix' => '', + 'schema' => 'public', + ]); + } + + private function getConnectionParams(): array + { + return [ + 'driver' => $GLOBALS['db_type'] ?? 'pdo_pgsql', + 'user' => $GLOBALS['db_username'], + 'password' => $GLOBALS['db_password'], + 'host' => $GLOBALS['db_host'], + 'database' => $GLOBALS['db_database'], + 'port' => $GLOBALS['db_port'], + ]; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 2467719..67c8ab0 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -9,7 +9,7 @@ abstract class TestCase extends BaseTestCase { - protected function getPackageProviders($app) + protected function getPackageProviders($app): array { return [UmbrellioPostgresProvider::class]; } diff --git a/tests/Unit/Doctrine/Types/TsRangeTypeTest.php b/tests/Unit/Doctrine/Types/TsRangeTypeTest.php new file mode 100644 index 0000000..375908d --- /dev/null +++ b/tests/Unit/Doctrine/Types/TsRangeTypeTest.php @@ -0,0 +1,74 @@ +type = $this + ->getMockBuilder(TsRangeType::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->abstractPlatform = $this->getMockForAbstractClass(AbstractPlatform::class); + } + + /** @test */ + public function getSQLDeclaration(): void + { + $this->assertSame(TsRangeType::TYPE_NAME, $this->type->getSQLDeclaration([], $this->abstractPlatform)); + } + + /** + * @dataProvider providePHPValues + * @test + */ + public function convertToPHPValue($value, $expected): void + { + $this->assertSame($expected, $this->type->convertToDatabaseValue($value, $this->abstractPlatform)); + } + + public function provideDatabaseValues(): Generator + { + yield [null, null]; + yield ['[1352302322,1352302356]', '[1352302322,1352302356]']; + } + + /** + * @dataProvider provideDatabaseValues + * @test + */ + public function convertToDatabaseValue($value, $expected): void + { + $this->assertSame($expected, $this->type->convertToPHPValue($value, $this->abstractPlatform)); + } + + public function providePHPValues(): Generator + { + yield [null, null]; + yield ['[1352302322,1352302356]', '[1352302322,1352302356]']; + } + + /** @test */ + public function getTypeName(): void + { + $this->assertSame(TsRangeType::TYPE_NAME, $this->type->getName()); + } +} diff --git a/tests/Unit/Doctrine/Types/TsVectorTypeTest.php b/tests/Unit/Doctrine/Types/TsVectorTypeTest.php new file mode 100644 index 0000000..10ba52b --- /dev/null +++ b/tests/Unit/Doctrine/Types/TsVectorTypeTest.php @@ -0,0 +1,74 @@ +type = $this + ->getMockBuilder(TsVectorType::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->abstractPlatform = $this->getMockForAbstractClass(AbstractPlatform::class); + } + + /** @test */ + public function getSQLDeclaration(): void + { + $this->assertSame(TsVectorType::TYPE_NAME, $this->type->getSQLDeclaration([], $this->abstractPlatform)); + } + + /** + * @dataProvider providePHPValues + * @test + */ + public function convertToPHPValue($value, $expected): void + { + $this->assertSame($expected, $this->type->convertToDatabaseValue($value, $this->abstractPlatform)); + } + + public function provideDatabaseValues(): Generator + { + yield [null, null]; + yield ['key:2, key:2,3', 'key:2, key:2,3']; + } + + /** + * @dataProvider provideDatabaseValues + * @test + */ + public function convertToDatabaseValue($value, $expected): void + { + $this->assertSame($expected, $this->type->convertToPHPValue($value, $this->abstractPlatform)); + } + + public function providePHPValues(): Generator + { + yield [null, null]; + yield ['key:2, key:2,3', 'key:2, key:2,3']; + } + + /** @test */ + public function getTypeName(): void + { + $this->assertSame(TsVectorType::TYPE_NAME, $this->type->getName()); + } +} diff --git a/tests/Unit/Extensions/AbstractExtensionTest.php b/tests/Unit/Extensions/AbstractExtensionTest.php new file mode 100644 index 0000000..24c1ee9 --- /dev/null +++ b/tests/Unit/Extensions/AbstractExtensionTest.php @@ -0,0 +1,65 @@ + new class() extends Model { + }, + ]; + } + }; + + $this->expectException(MixinInvalidException::class); + + /** @var AbstractExtension $abstractExtension */ + $abstractExtension::register(); + } + + /** @test */ + public function registerWithInvalidMixin(): void + { + $abstractExtension = new class() extends AbstractExtension { + public static function getName(): string + { + return 'extension'; + } + + public static function getMixins(): array + { + return [ + ServiceProvider::class => new class() extends AbstractComponent { + }, + ]; + } + }; + + $this->expectException(MacroableMissedException::class); + + /** @var AbstractExtension $abstractExtension */ + $abstractExtension::register(); + } +} diff --git a/tests/Unit/Helpers/BlueprintAssertions.php b/tests/Unit/Helpers/BlueprintAssertions.php new file mode 100644 index 0000000..be7de4b --- /dev/null +++ b/tests/Unit/Helpers/BlueprintAssertions.php @@ -0,0 +1,51 @@ +blueprint = new Blueprint($table); + $this->postgresConnection = $this->createMock(PostgresConnection::class); + $this->postgresGrammar = new PostgresGrammar(); + } + + /** + * @param string|array $sql + */ + protected function assertSameSql($sql): void + { + $this->assertSame((array) $sql, $this->runToSql()); + } + + protected function assertRegExpSql(string $regexpExpected): void + { + foreach ($this->runToSql() as $sql) { + $this->assertRegExp($regexpExpected, $sql); + } + } + + private function runToSql(): array + { + return $this->blueprint->toSql($this->postgresConnection, $this->postgresGrammar); + } +} diff --git a/tests/Unit/Schema/BlueprintTest.php b/tests/Unit/Schema/Blueprint/PartitionTest.php similarity index 64% rename from tests/Unit/Schema/BlueprintTest.php rename to tests/Unit/Schema/Blueprint/PartitionTest.php index e70633f..e748882 100644 --- a/tests/Unit/Schema/BlueprintTest.php +++ b/tests/Unit/Schema/Blueprint/PartitionTest.php @@ -2,32 +2,29 @@ declare(strict_types=1); -namespace Umbrellio\Postgres\Unit\Schema; +namespace Umbrellio\Postgres\Unit\Schema\Blueprint; use Illuminate\Support\Carbon; use InvalidArgumentException; -use Umbrellio\Postgres\PostgresConnection; -use Umbrellio\Postgres\Schema\Blueprint; -use Umbrellio\Postgres\Schema\Grammars\PostgresGrammar; use Umbrellio\Postgres\Tests\TestCase; +use Umbrellio\Postgres\Tests\Unit\Helpers\BlueprintAssertions; -class BlueprintTest extends TestCase +class PartitionTest extends TestCase { - /** @var Blueprint */ - private $blueprint; + use BlueprintAssertions; + + private const TABLE = 'test_table'; protected function setUp(): void { parent::setUp(); - - $this->blueprint = new Blueprint('test_table'); + $this->initializeMock(static::TABLE); } /** @test */ public function detachPartition(): void { $this->blueprint->detachPartition('some_partition'); - $this->assertSameSql('alter table "test_table" detach partition some_partition'); } @@ -38,7 +35,6 @@ public function attachPartitionRangeInt(): void 'from' => 10, 'to' => 100, ]); - $this->assertSameSql('alter table "test_table" attach partition some_partition for values from (10) to (100)'); } @@ -46,7 +42,6 @@ public function attachPartitionRangeInt(): void public function attachPartitionFailedWithoutForValuesPart(): void { $this->blueprint->attachPartition('some_partition'); - $this->expectException(InvalidArgumentException::class); $this->runToSql(); } @@ -61,18 +56,10 @@ public function attachPartitionRangeDates(): void 'to' => $tomorrow, ]); - $this->assertSameSql( - 'alter table "test_table" attach partition some_partition ' - . "for values from ('{$today->toDateTimeString()}') to ('{$tomorrow->toDateTimeString()}')"); - } - - private function assertSameSql(string $sql): void - { - $this->assertSame([$sql], $this->runToSql()); - } - - private function runToSql(): array - { - return $this->blueprint->toSql($this->createMock(PostgresConnection::class), new PostgresGrammar()); + $this->assertSameSql(sprintf( + 'alter table "test_table" attach partition some_partition for values from (\'%s\') to (\'%s\')', + $today->toDateTimeString(), + $tomorrow->toDateTimeString() + )); } } diff --git a/tests/Unit/Schema/Grammars/GrammarTest.php b/tests/Unit/Schema/Grammars/GrammarTest.php index 808628b..2a87d54 100644 --- a/tests/Unit/Schema/Grammars/GrammarTest.php +++ b/tests/Unit/Schema/Grammars/GrammarTest.php @@ -4,43 +4,33 @@ namespace Umbrellio\Postgres\Tests\Unit\Schema\Grammars; -use Mockery; -use Umbrellio\Postgres\PostgresConnection; -use Umbrellio\Postgres\Schema\Blueprint; -use Umbrellio\Postgres\Schema\Grammars\PostgresGrammar; use Umbrellio\Postgres\Tests\TestCase; +use Umbrellio\Postgres\Tests\Unit\Helpers\BlueprintAssertions; class GrammarTest extends TestCase { - /** @test */ - public function addingGinIndex() - { - $blueprint = new Blueprint('test'); - $blueprint->gin('foo'); - $statements = $blueprint->toSql($this->getConnectionMock(), $this->getGrammar()); - $this->assertCount(1, $statements); - $this->assertStringContainsString('CREATE INDEX', $statements[0]); - $this->assertStringContainsString('GIN("foo")', $statements[0]); - } + use BlueprintAssertions; - /** @test */ - public function addingGistIndex() + private const TABLE = 'test_table'; + + protected function setUp(): void { - $blueprint = new Blueprint('test'); - $blueprint->gist('foo'); - $statements = $blueprint->toSql($this->getConnectionMock(), $this->getGrammar()); - $this->assertCount(1, $statements); - $this->assertStringContainsString('CREATE INDEX', $statements[0]); - $this->assertStringContainsString('GIST("foo")', $statements[0]); + parent::setUp(); + + $this->initializeMock(static::TABLE); } - protected function getConnectionMock() + /** @test */ + public function addingGinIndex(): void { - return Mockery::mock(PostgresConnection::class); + $this->blueprint->gin('foo'); + $this->assertRegExpSql('/CREATE INDEX test_table_foo_gin ON (public.)?"test_table" USING GIN\("foo"\)/'); } - protected function getGrammar() + /** @test */ + public function addingGistIndex(): void { - return new PostgresGrammar(); + $this->blueprint->gist('foo'); + $this->assertRegExpSql('/CREATE INDEX test_table_foo_gist ON (public.)?"test_table" USING GIST\("foo"\)/'); } } diff --git a/tests/Unit/Schema/IndexTest.php b/tests/Unit/Schema/IndexTest.php deleted file mode 100644 index 1b05de6..0000000 --- a/tests/Unit/Schema/IndexTest.php +++ /dev/null @@ -1,42 +0,0 @@ -blueprint = Mockery::mock(Blueprint::class) - ->makePartial() - ->shouldAllowMockingProtectedMethods(); - } - - /** @test */ - public function ginIndex() - { - $this->blueprint - ->shouldReceive('indexCommand') - ->with('gin', 'col', 'myName'); - $this->blueprint->gin('col', 'myName'); - } - - /** @test */ - public function gistIndex() - { - $this->blueprint - ->shouldReceive('indexCommand') - ->with('gist', 'col', 'myName'); - $this->blueprint->gist('col', 'myName'); - } -} diff --git a/tests/travis/install-postgres-10.sh b/tests/travis/install-postgres-10.sh new file mode 100755 index 0000000..8804124 --- /dev/null +++ b/tests/travis/install-postgres-10.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +set -ex + +echo "Installing Postgres 10" +sudo service postgresql stop +sudo apt-get remove -q 'postgresql-*' +sudo apt-get update -q +sudo apt-get install -q postgresql-10 postgresql-client-10 +sudo cp /etc/postgresql/{9.6,10}/main/pg_hba.conf + +echo "Restarting Postgres 10" +sudo service postgresql restart diff --git a/tests/travis/install-postgres-11.sh b/tests/travis/install-postgres-11.sh new file mode 100755 index 0000000..2ef1aab --- /dev/null +++ b/tests/travis/install-postgres-11.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -ex + +echo "Preparing Postgres 11" + +sudo service postgresql stop || true + +sudo docker run -d --name postgres11 -p 5432:5432 postgres:11.1 +sudo docker exec -i postgres11 bash <<< 'until pg_isready -U postgres > /dev/null 2>&1 ; do sleep 1; done' + +echo "Postgres 11 ready"