diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbd0e23e0..089c50fcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,6 @@ on: push: branches: - '0.x' - - '0.next' - '1.x' pull_request: @@ -64,12 +63,10 @@ jobs: - name: Composer install run: | - if [[ ${{ matrix.php-version }} == '8.2' ]]; then - composer install --ignore-platform-req=php - elif ${{ matrix.prefer-lowest == 'prefer-lowest' }}; then + if ${{ matrix.prefer-lowest == 'prefer-lowest' }}; then composer update --prefer-lowest --prefer-stable else - composer update + composer install fi - name: Setup problem matchers for PHPUnit @@ -96,7 +93,7 @@ jobs: if [[ ${{ matrix.db-type }} == 'mysql' ]]; then export MYSQL_DSN='mysql://root:root@127.0.0.1/phinx'; fi if [[ ${{ matrix.db-type }} == 'pgsql' ]]; then export PGSQL_DSN='pgsql://postgres:postgres@127.0.0.1/phinx'; fi - if [[ ${{ matrix.php-version }} == '8.1' ]]; then + if [[ ${{ matrix.php-version }} == '8.2' ]]; then export CODECOVERAGE=1 && vendor/bin/phpunit --verbose --coverage-clover=coverage.xml else vendor/bin/phpunit @@ -107,7 +104,7 @@ jobs: run: composer require --dev dereuromark/composer-prefer-lowest && vendor/bin/validate-prefer-lowest -m - name: Submit code coverage - if: matrix.php-version == '8.1' + if: matrix.php-version == '8.2' uses: codecov/codecov-action@v3 testsuite-windows: @@ -209,7 +206,7 @@ jobs: run: composer stan-setup - name: Run PHP CodeSniffer - run: vendor/bin/phpcs --report=checkstyle -np app/ src/ tests/ + run: vendor/bin/phpcs - name: Run phpstan if: always() diff --git a/Dockerfile b/Dockerfile index 4a9dbd7cb..c01e191cc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM php:7.2 +FROM php:7.3 # system dependecies RUN apt-get update && apt-get install -y \ diff --git a/LICENSE b/LICENSE index 983a90682..54619b91f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,9 +1,23 @@ (The MIT license) -Copyright (c) 2012-present Rob Morgan +Copyright (c) 2012-2017 Rob Morgan +Copyright (c) 2017-present, Cake Software Foundation, Inc. (https://cakefoundation.org) -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 51bcc8cd2..ddecc7ab0 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # [Phinx](https://phinx.org): Simple PHP Database Migrations -[![Build Status](https://github.com/cakephp/phinx/workflows/Phinx%20CI/badge.svg?branch=master&event=push)](https://github.com/cakephp/phinx/actions?query=workflow%3A%22Phinx+CI%22+branch%3Amaster+event%3Apush) +[![Build Status](https://github.com/cakephp/phinx/workflows/CI/badge.svg?branch=master&event=push)](https://github.com/cakephp/phinx/actions?query=workflow%3A%22CI%22+branch%3Amaster+event%3Apush) [![Code Coverage](https://codecov.io/gh/cakephp/phinx/branch/master/graph/badge.svg)](https://codecov.io/gh/cakephp/phinx) [![Latest Stable Version](https://poser.pugx.org/robmorgan/phinx/version.png)](https://packagist.org/packages/robmorgan/phinx) -[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.2-8892BF.svg)](https://php.net/) +[![Minimum PHP Version](https://img.shields.io/badge/php-%3E%3D%207.3-8892BF.svg)](https://php.net/) [![Total Downloads](https://poser.pugx.org/robmorgan/phinx/d/total.png)](https://packagist.org/packages/robmorgan/phinx) ## Intro diff --git a/composer.json b/composer.json index 47377ab64..dfec5a38c 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ } ], "require": { - "php": ">=8.1", + "php-64bit": ">=8.1", "cakephp/database": "5.x-dev", "psr/container": "^2.0", "symfony/console": "^6.0", @@ -71,9 +71,9 @@ "@test", "@cs-check" ], - "cs-check": "phpcs -np app/ src/ tests/", - "cs-fix": "phpcbf -np app/ src/ tests/", - "stan": "phpstan analyse src/", + "cs-check": "phpcs", + "cs-fix": "phpcbf", + "stan": "phpstan analyse", "stan-setup": "cp composer.json composer.backup && composer require --dev phpstan/phpstan:~1.9.0 && mv composer.backup composer.json", "test": "phpunit --colors=always" }, diff --git a/docs/en/configuration.rst b/docs/en/configuration.rst index bf53a4e92..05ff795e9 100644 --- a/docs/en/configuration.rst +++ b/docs/en/configuration.rst @@ -512,3 +512,23 @@ Within the bootstrap script, the following variables will be available: * @var \Symfony\Component\Console\Output\OutputInterface $output The executing command's output object * @var \Phinx\Console\Command\AbstractCommand $context the executing command object */ + +Feature Flags +------------- + +For some breaking changes, Phinx offers a way to opt-out of new behavior. The following flags are available: + +* ``unsigned_primary_keys``: Should Phinx create primary keys as unsigned integers? (default: ``true``) +* ``column_null_default``: Should Phinx create columns as null by default? (default: ``true``) + +.. code-block:: yaml + + feature_flags: + unsigned_primary_keys: false + +These values can also be set by modifying class fields on the ```Phinx\Config\FeatureFlags``` class, converting +the flag name to ``camelCase``, for example: + +.. code-block:: php + + Phinx\Config\FeatureFlags::$unsignedPrimaryKeys = false; diff --git a/docs/en/index.rst b/docs/en/index.rst index 530c8c965..c17323068 100644 --- a/docs/en/index.rst +++ b/docs/en/index.rst @@ -3,7 +3,12 @@ Phinx Documentation =================== -Phinx makes it ridiculously easy to manage the database migrations for your PHP app. In less than 5 minutes, you can install Phinx using Composer and create your first database migration. Phinx is just about migrations without all the bloat of a database ORM system or application framework. +Phinx makes it ridiculously easy to manage the database migrations for your PHP +app. In less than 5 minutes, you can install Phinx using Composer and create +your first database migration. Phinx is just about migrations without all the +bloat of a database ORM system or application framework. + +Phinx requires a 64-bit version of PHP to function. Contents ======== diff --git a/docs/en/migrations.rst b/docs/en/migrations.rst index 4ebb074f3..3710d3ff1 100644 --- a/docs/en/migrations.rst +++ b/docs/en/migrations.rst @@ -902,11 +902,8 @@ Limit Option and MySQL When using the MySQL adapter, there are a couple things to consider when working with limits: -- When using a ``string`` primary key or index on MySQL 5.7 or below and the default charset of ``utf8mb4_unicode_ci``, you -must specify a limit less than or equal to 191, or use a different charset. -- Additional hinting of database column type can be -made for ``integer``, ``text``, ``blob``, ``tinyblob``, ``mediumblob``, ``longblob`` columns. Using ``limit`` with -one the following options will modify the column type accordingly: +- When using a ``string`` primary key or index on MySQL 5.7 or below, or the MyISAM storage engine, and the default charset of ``utf8mb4_unicode_ci``, you must specify a limit less than or equal to 191, or use a different charset. +- Additional hinting of database column type can be made for ``integer``, ``text``, ``blob``, ``tinyblob``, ``mediumblob``, ``longblob`` columns. Using ``limit`` with one the following options will modify the column type accordingly: ============ ============== Limit Column Type diff --git a/phpcs.xml b/phpcs.xml index c0f0c06ff..7bdb4f504 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -2,11 +2,16 @@ + + + src/ + tests/ + + *_files* + 0 - - *_files* diff --git a/src/Phinx/Config/Config.php b/src/Phinx/Config/Config.php index 0eb92249f..91cfbd499 100644 --- a/src/Phinx/Config/Config.php +++ b/src/Phinx/Config/Config.php @@ -57,6 +57,10 @@ public function __construct(array $configArray, ?string $configFilePath = null) { $this->configFilePath = $configFilePath; $this->values = $this->replaceTokens($configArray); + + if (isset($this->values['feature_flags'])) { + FeatureFlags::setFlagsFromConfig($this->values['feature_flags']); + } } /** diff --git a/src/Phinx/Config/FeatureFlags.php b/src/Phinx/Config/FeatureFlags.php new file mode 100644 index 000000000..dded535b8 --- /dev/null +++ b/src/Phinx/Config/FeatureFlags.php @@ -0,0 +1,41 @@ +addColumn('migration_name', 'string', ['limit' => 100, 'default' => null, 'null' => true]) ->addColumn('start_time', 'timestamp', ['default' => null, 'null' => true]) ->addColumn('end_time', 'timestamp', ['default' => null, 'null' => true]) - ->addColumn('breakpoint', 'boolean', ['default' => false]) + ->addColumn('breakpoint', 'boolean', ['default' => false, 'null' => false]) ->save(); } catch (Exception $exception) { throw new InvalidArgumentException( diff --git a/src/Phinx/Db/Adapter/MysqlAdapter.php b/src/Phinx/Db/Adapter/MysqlAdapter.php index 77246c4ad..75ba2f1d2 100644 --- a/src/Phinx/Db/Adapter/MysqlAdapter.php +++ b/src/Phinx/Db/Adapter/MysqlAdapter.php @@ -11,6 +11,7 @@ use Cake\Database\Driver\Mysql as MysqlDriver; use InvalidArgumentException; use PDO; +use Phinx\Config\FeatureFlags; use Phinx\Db\Table\Column; use Phinx\Db\Table\ForeignKey; use Phinx\Db\Table\Index; @@ -285,7 +286,7 @@ public function createTable(Table $table, array $columns = [], array $indexes = $column->setName($options['id']) ->setType('integer') ->setOptions([ - 'signed' => $options['signed'] ?? false, + 'signed' => $options['signed'] ?? !FeatureFlags::$unsignedPrimaryKeys, 'identity' => true, ]); @@ -454,7 +455,7 @@ public function truncateTable(string $tableName): void public function getColumns(string $tableName): array { $columns = []; - $rows = $this->fetchAll(sprintf('SHOW COLUMNS FROM %s', $this->quoteTableName($tableName))); + $rows = $this->fetchAll(sprintf('SHOW FULL COLUMNS FROM %s', $this->quoteTableName($tableName))); foreach ($rows as $columnInfo) { $phinxType = $this->getPhinxType($columnInfo['Type']); @@ -464,7 +465,8 @@ public function getColumns(string $tableName): array ->setType($phinxType['name']) ->setSigned(strpos($columnInfo['Type'], 'unsigned') === false) ->setLimit($phinxType['limit']) - ->setScale($phinxType['scale']); + ->setScale($phinxType['scale']) + ->setComment($columnInfo['Comment']); if ($columnInfo['Extra'] === 'auto_increment') { $column->setIdentity(true); @@ -928,7 +930,7 @@ protected function getDropForeignKeyByColumnsInstructions(string $tableName, arr if (empty($instructions->getAlterParts())) { throw new InvalidArgumentException(sprintf( - "Not foreign key on columns '%s' exist", + "No foreign key on column(s) '%s' exists", implode(',', $columns) )); } diff --git a/src/Phinx/Db/Adapter/PdoAdapter.php b/src/Phinx/Db/Adapter/PdoAdapter.php index bb30ccf1f..90838925a 100644 --- a/src/Phinx/Db/Adapter/PdoAdapter.php +++ b/src/Phinx/Db/Adapter/PdoAdapter.php @@ -631,7 +631,7 @@ protected function getDefaultValueDefinition($default, ?string $columnType = nul { if ($default instanceof Literal) { $default = (string)$default; - } elseif (is_string($default) && strpos($default, 'CURRENT_TIMESTAMP') !== 0) { + } elseif (is_string($default) && stripos($default, 'CURRENT_TIMESTAMP') !== 0) { // Ensure a defaults of CURRENT_TIMESTAMP(3) is not quoted. $default = $this->getConnection()->quote($default); } elseif (is_bool($default)) { diff --git a/src/Phinx/Db/Adapter/SQLiteAdapter.php b/src/Phinx/Db/Adapter/SQLiteAdapter.php index 915433744..7fc66292d 100644 --- a/src/Phinx/Db/Adapter/SQLiteAdapter.php +++ b/src/Phinx/Db/Adapter/SQLiteAdapter.php @@ -734,7 +734,7 @@ protected function getAddColumnInstructions(Table $table, Column $column): Alter return $newState + $state; }); - return $this->copyAndDropTmpTable($instructions, $tableName); + return $this->endAlterByCopyTable($instructions, $tableName); } /** @@ -793,6 +793,236 @@ protected function getDeclaringIndexSql(string $tableName, string $indexName): s return $sql; } + /** + * Obtains index and trigger information for a table. + * + * They will be stored in the state as arrays under the `indices` and `triggers` + * keys accordingly. + * + * Index columns defined as expressions, as for example in `ON (ABS(id), other)`, + * will appear as `null`, so for the given example the columns for the index would + * look like `[null, 'other']`. + * + * @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to modify + * @param string $tableName The name of table being processed + * @return \Phinx\Db\Util\AlterInstructions + */ + protected function bufferIndicesAndTriggers(AlterInstructions $instructions, string $tableName): AlterInstructions + { + $instructions->addPostStep(function (array $state) use ($tableName): array { + $state['indices'] = []; + $state['triggers'] = []; + + $rows = $this->fetchAll( + sprintf( + " + SELECT * + FROM sqlite_master + WHERE + (`type` = 'index' OR `type` = 'trigger') + AND tbl_name = %s + AND sql IS NOT NULL + ", + $this->quoteValue($tableName) + ) + ); + + $schema = $this->getSchemaName($tableName, true)['schema']; + + foreach ($rows as $row) { + switch ($row['type']) { + case 'index': + $info = $this->fetchAll( + sprintf('PRAGMA %sindex_info(%s)', $schema, $this->quoteValue($row['name'])) + ); + + $columns = array_map( + function ($column) { + if ($column === null) { + return null; + } + + return strtolower($column); + }, + array_column($info, 'name') + ); + $hasExpressions = in_array(null, $columns, true); + + $index = [ + 'columns' => $columns, + 'hasExpressions' => $hasExpressions, + ]; + + $state['indices'][] = $index + $row; + break; + + case 'trigger': + $state['triggers'][] = $row; + break; + } + } + + return $state; + }); + + return $instructions; + } + + /** + * Filters out indices that reference a removed column. + * + * @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to modify + * @param string $columnName The name of the removed column + * @return \Phinx\Db\Util\AlterInstructions + */ + protected function filterIndicesForRemovedColumn( + AlterInstructions $instructions, + string $columnName + ): AlterInstructions { + $instructions->addPostStep(function (array $state) use ($columnName): array { + foreach ($state['indices'] as $key => $index) { + if ( + !$index['hasExpressions'] && + in_array(strtolower($columnName), $index['columns'], true) + ) { + unset($state['indices'][$key]); + } + } + + return $state; + }); + + return $instructions; + } + + /** + * Updates indices that reference a renamed column. + * + * @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to modify + * @param string $oldColumnName The old column name + * @param string $newColumnName The new column name + * @return \Phinx\Db\Util\AlterInstructions + */ + protected function updateIndicesForRenamedColumn( + AlterInstructions $instructions, + string $oldColumnName, + string $newColumnName + ): AlterInstructions { + $instructions->addPostStep(function (array $state) use ($oldColumnName, $newColumnName): array { + foreach ($state['indices'] as $key => $index) { + if ( + !$index['hasExpressions'] && + in_array(strtolower($oldColumnName), $index['columns'], true) + ) { + $pattern = ' + / + (INDEX.+?ON\s.+?) + (\(\s*|,\s*) # opening parenthesis or comma + (?:`|"|\[)? # optional opening quote + (%s) # column name + (?:`|"|\])? # optional closing quote + (\s+COLLATE\s+.+?)? # optional collation + (\s+(?:ASC|DESC))? # optional order + (\s*,|\s*\)) # comma or closing parenthesis + /isx'; + + $newColumnName = $this->quoteColumnName($newColumnName); + + $state['indices'][$key]['sql'] = preg_replace( + sprintf($pattern, preg_quote($oldColumnName, '/')), + "\\1\\2$newColumnName\\4\\5\\6", + $index['sql'] + ); + } + } + + return $state; + }); + + return $instructions; + } + + /** + * Recreates indices and triggers. + * + * @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to process + * @return \Phinx\Db\Util\AlterInstructions + */ + protected function recreateIndicesAndTriggers(AlterInstructions $instructions): AlterInstructions + { + $instructions->addPostStep(function (array $state): array { + foreach ($state['indices'] as $index) { + $this->execute($index['sql']); + } + + foreach ($state['triggers'] as $trigger) { + $this->execute($trigger['sql']); + } + + return $state; + }); + + return $instructions; + } + + /** + * Returns instructions for validating the foreign key constraints of + * the given table, and of those tables whose constraints are + * targeting it. + * + * @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to process + * @param string $tableName The name of the table for which to check constraints. + * @return \Phinx\Db\Util\AlterInstructions + */ + protected function validateForeignKeys(AlterInstructions $instructions, string $tableName): AlterInstructions + { + $instructions->addPostStep(function ($state) use ($tableName) { + $tablesToCheck = [ + $tableName, + ]; + + $otherTables = $this + ->query( + "SELECT name FROM sqlite_master WHERE type = 'table' AND name != ?", + [$tableName] + ) + ->fetchAll(); + + foreach ($otherTables as $otherTable) { + $foreignKeyList = $this->getTableInfo($otherTable['name'], 'foreign_key_list'); + foreach ($foreignKeyList as $foreignKey) { + if (strcasecmp($foreignKey['table'], $tableName) === 0) { + $tablesToCheck[] = $otherTable['name']; + break; + } + } + } + + $tablesToCheck = array_unique(array_map('strtolower', $tablesToCheck)); + + foreach ($tablesToCheck as $tableToCheck) { + $schema = $this->getSchemaName($tableToCheck, true)['schema']; + + $stmt = $this->query( + sprintf('PRAGMA %sforeign_key_check(%s)', $schema, $this->quoteTableName($tableToCheck)) + ); + $row = $stmt->fetch(); + $stmt->closeCursor(); + + if (is_array($row)) { + throw new RuntimeException(sprintf( + 'Integrity constraint violation: FOREIGN KEY constraint on `%s` failed.', + $tableToCheck + )); + } + } + + return $state; + }); + + return $instructions; + } + /** * Copies all the data from a tmp table to another table * @@ -832,20 +1062,6 @@ protected function copyAndDropTmpTable(AlterInstructions $instructions, string $ $state['selectColumns'] ); - $rows = $this->fetchAll( - sprintf( - " - SELECT * - FROM sqlite_master - WHERE - (`type` = 'index' OR `type` = 'trigger') - AND tbl_name = %s - AND sql IS NOT NULL - ", - $this->quoteValue($tableName) - ) - ); - $this->execute(sprintf('DROP TABLE %s', $this->quoteTableName($tableName))); $this->execute(sprintf( 'ALTER TABLE %s RENAME TO %s', @@ -853,10 +1069,6 @@ protected function copyAndDropTmpTable(AlterInstructions $instructions, string $ $this->quoteTableName($tableName) )); - foreach ($rows as $row) { - $this->execute($row['sql']); - } - return $state; }); @@ -945,6 +1157,59 @@ protected function beginAlterByCopyTable(string $tableName): AlterInstructions return $instructions; } + /** + * Returns the final instructions to alter a table using the + * create-copy-drop strategy. + * + * @param \Phinx\Db\Util\AlterInstructions $instructions The instructions to modify + * @param string $tableName The name of table being processed + * @param ?string $renamedOrRemovedColumnName The name of the renamed or removed column when part of a column + * rename/drop operation. + * @param ?string $newColumnName The new column name when part of a column rename operation. + * @param bool $validateForeignKeys Whether to validate foreign keys after the copy and drop operations. Note that + * enabling this option only has an effect when the `foreign_keys` PRAGMA is set to `ON`! + * @return \Phinx\Db\Util\AlterInstructions + */ + protected function endAlterByCopyTable( + AlterInstructions $instructions, + string $tableName, + ?string $renamedOrRemovedColumnName = null, + ?string $newColumnName = null, + bool $validateForeignKeys = true + ): AlterInstructions { + $instructions = $this->bufferIndicesAndTriggers($instructions, $tableName); + + if ($renamedOrRemovedColumnName !== null) { + if ($newColumnName !== null) { + $this->updateIndicesForRenamedColumn($instructions, $renamedOrRemovedColumnName, $newColumnName); + } else { + $this->filterIndicesForRemovedColumn($instructions, $renamedOrRemovedColumnName); + } + } + + $foreignKeysEnabled = (bool)$this->fetchRow('PRAGMA foreign_keys')['foreign_keys']; + + if ($foreignKeysEnabled) { + $instructions->addPostStep('PRAGMA foreign_keys = OFF'); + } + + $instructions = $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->recreateIndicesAndTriggers($instructions); + + if ($foreignKeysEnabled) { + $instructions->addPostStep('PRAGMA foreign_keys = ON'); + } + + if ( + $foreignKeysEnabled && + $validateForeignKeys + ) { + $instructions = $this->validateForeignKeys($instructions, $tableName); + } + + return $instructions; + } + /** * @inheritDoc */ @@ -969,7 +1234,7 @@ protected function getRenameColumnInstructions(string $tableName, string $column return $newState + $state; }); - return $this->copyAndDropTmpTable($instructions, $tableName); + return $this->endAlterByCopyTable($instructions, $tableName, $columnName, $newColumnName); } /** @@ -998,7 +1263,7 @@ protected function getChangeColumnInstructions(string $tableName, string $column return $newState + $state; }); - return $this->copyAndDropTmpTable($instructions, $tableName); + return $this->endAlterByCopyTable($instructions, $tableName); } /** @@ -1030,7 +1295,7 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa return $state; }); - return $this->copyAndDropTmpTable($instructions, $tableName); + return $this->endAlterByCopyTable($instructions, $tableName, $columnName); } /** @@ -1306,7 +1571,7 @@ protected function getAddPrimaryKeyInstructions(Table $table, string $column): A return compact('selectColumns', 'writeColumns') + $state; }); - return $this->copyAndDropTmpTable($instructions, $tableName); + return $this->endAlterByCopyTable($instructions, $tableName); } /** @@ -1316,7 +1581,8 @@ protected function getAddPrimaryKeyInstructions(Table $table, string $column): A */ protected function getDropPrimaryKeyInstructions(Table $table, string $column): AlterInstructions { - $instructions = $this->beginAlterByCopyTable($table->getName()); + $tableName = $table->getName(); + $instructions = $this->beginAlterByCopyTable($tableName); $instructions->addPostStep(function ($state) { $search = "/(,?\s*PRIMARY KEY\s*\([^\)]*\)|\s+PRIMARY KEY(\s+AUTOINCREMENT)?)/"; @@ -1335,7 +1601,7 @@ protected function getDropPrimaryKeyInstructions(Table $table, string $column): return $newState + $state; }); - return $this->copyAndDropTmpTable($instructions, $table->getName()); + return $this->endAlterByCopyTable($instructions, $tableName, null, null, false); } /** @@ -1383,7 +1649,7 @@ protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreig return compact('selectColumns', 'writeColumns') + $state; }); - return $this->copyAndDropTmpTable($instructions, $tableName); + return $this->endAlterByCopyTable($instructions, $tableName); } /** @@ -1441,7 +1707,7 @@ protected function getDropForeignKeyByColumnsInstructions(string $tableName, arr return $newState + $state; }); - return $this->copyAndDropTmpTable($instructions, $tableName); + return $this->endAlterByCopyTable($instructions, $tableName); } /** diff --git a/src/Phinx/Db/Adapter/SqlServerAdapter.php b/src/Phinx/Db/Adapter/SqlServerAdapter.php index 9a34bef90..5493d9c29 100644 --- a/src/Phinx/Db/Adapter/SqlServerAdapter.php +++ b/src/Phinx/Db/Adapter/SqlServerAdapter.php @@ -1018,7 +1018,7 @@ protected function getDropForeignKeyInstructions(string $tableName, string $cons $instructions->addPostStep(sprintf( 'ALTER TABLE %s DROP CONSTRAINT %s', $this->quoteTableName($tableName), - $constraint + $this->quoteColumnName($constraint) )); return $instructions; diff --git a/src/Phinx/Db/Table.php b/src/Phinx/Db/Table.php index 518716f5b..e532517fc 100644 --- a/src/Phinx/Db/Table.php +++ b/src/Phinx/Db/Table.php @@ -26,6 +26,7 @@ use Phinx\Db\Plan\Plan; use Phinx\Db\Table\Column; use Phinx\Db\Table\Table as TableValue; +use Phinx\Util\Literal; use RuntimeException; /** @@ -299,8 +300,10 @@ public function addColumn($columnName, $type = null, array $options = []) { if ($columnName instanceof Column) { $action = new AddColumn($this->table, $columnName); - } else { + } elseif ($type instanceof Literal) { $action = AddColumn::build($this->table, $columnName, $type, $options); + } else { + $action = new AddColumn($this->table, $this->getAdapter()->getColumnForType($columnName, $type, $options)); } // Delegate to Adapters to check column type diff --git a/src/Phinx/Db/Table/Column.php b/src/Phinx/Db/Table/Column.php index deae90a7f..f70ffa7a6 100644 --- a/src/Phinx/Db/Table/Column.php +++ b/src/Phinx/Db/Table/Column.php @@ -7,6 +7,7 @@ namespace Phinx\Db\Table; +use Phinx\Config\FeatureFlags; use Phinx\Db\Adapter\AdapterInterface; use Phinx\Db\Adapter\PostgresAdapter; use RuntimeException; @@ -158,6 +159,14 @@ class Column */ protected $values; + /** + * Column constructor + */ + public function __construct() + { + $this->null = FeatureFlags::$columnNullDefault; + } + /** * Sets the column name. * diff --git a/src/Phinx/Migration/Manager.php b/src/Phinx/Migration/Manager.php index 4454c541d..76b68a032 100644 --- a/src/Phinx/Migration/Manager.php +++ b/src/Phinx/Migration/Manager.php @@ -504,6 +504,7 @@ public function rollback(string $environment, $target = null, bool $force = fals // if we have a date (ie. the target must not match a version) and we are sorting by execution time, we // convert the version start time so we can compare directly with the target date if (!$this->getConfig()->isVersionOrderCreationTime() && !$targetMustMatchVersion) { + /** @var \DateTime $dateTime */ $dateTime = DateTime::createFromFormat('Y-m-d H:i:s', $executedVersion['start_time']); $executedVersion['start_time'] = $dateTime->format('YmdHis'); } diff --git a/src/Phinx/Migration/Manager/Environment.php b/src/Phinx/Migration/Manager/Environment.php index d742e7cd4..9a65b3fa9 100644 --- a/src/Phinx/Migration/Manager/Environment.php +++ b/src/Phinx/Migration/Manager/Environment.php @@ -111,16 +111,15 @@ public function executeMigration(MigrationInterface $migration, string $directio $migration->{$direction}(); } + // Record it in the database + $this->getAdapter()->migrated($migration, $direction, date('Y-m-d H:i:s', $startTime), date('Y-m-d H:i:s', time())); + // commit the transaction if the adapter supports it if ($this->getAdapter()->hasTransactions()) { $this->getAdapter()->commitTransaction(); } } - $migration->postFlightCheck(); - - // Record it in the database - $this->getAdapter()->migrated($migration, $direction, date('Y-m-d H:i:s', $startTime), date('Y-m-d H:i:s', time())); } /** diff --git a/src/Phinx/Migration/Migration.change.template.php.dist b/src/Phinx/Migration/Migration.change.template.php.dist index 53d56425a..40b609baf 100644 --- a/src/Phinx/Migration/Migration.change.template.php.dist +++ b/src/Phinx/Migration/Migration.change.template.php.dist @@ -1,4 +1,5 @@ false, + 'column_null_default' => false, + ]; + $this->assertTrue(FeatureFlags::$unsignedPrimaryKeys); + $this->assertTrue(FeatureFlags::$columnNullDefault); + FeatureFlags::setFlagsFromConfig($config); + $this->assertFalse(FeatureFlags::$unsignedPrimaryKeys); + $this->assertFalse(FeatureFlags::$columnNullDefault); + } +} diff --git a/tests/Phinx/Console/Command/CreateTest.php b/tests/Phinx/Console/Command/CreateTest.php index a0cb74e30..aa45df5fd 100644 --- a/tests/Phinx/Console/Command/CreateTest.php +++ b/tests/Phinx/Console/Command/CreateTest.php @@ -97,7 +97,7 @@ public function testCreateMigrationDefault(): void $this->assertMatchesRegularExpression('/^[0-9]{14}.php/', $fileName); $date = substr($fileName, 0, 14); $this->assertFileExists($this->config->getMigrationPaths()[0]); - $prefix = "config->getMigrationPaths()[0] . DIRECTORY_SEPARATOR . $fileName); $this->assertStringStartsWith($prefix, $migrationContents); $this->assertStringContainsString('public function change()', $migrationContents); @@ -130,7 +130,7 @@ public function testCreateMigrationWithName(): void $fileName = current($files); $this->assertMatchesRegularExpression('/^[0-9]{14}_my_migration.php/', $fileName); $this->assertFileExists($this->config->getMigrationPaths()[0]); - $prefix = "assertStringStartsWith($prefix, file_get_contents($this->config->getMigrationPaths()[0] . DIRECTORY_SEPARATOR . $fileName)); } @@ -484,7 +484,7 @@ public function testCreateMigrationWithChangeStyleInConfig(): void $this->assertMatchesRegularExpression('/^[0-9]{14}.php/', $fileName); $date = substr($fileName, 0, 14); $this->assertFileExists($this->config->getMigrationPaths()[0]); - $prefix = "config->getMigrationPaths()[0] . DIRECTORY_SEPARATOR . $fileName); $this->assertStringStartsWith($prefix, $migrationContents); $this->assertStringContainsString('public function change()', $migrationContents); @@ -518,7 +518,7 @@ public function testCreateMigrationWithChangeStyleAsFlag(): void $this->assertMatchesRegularExpression('/^[0-9]{14}.php/', $fileName); $date = substr($fileName, 0, 14); $this->assertFileExists($this->config->getMigrationPaths()[0]); - $prefix = "config->getMigrationPaths()[0] . DIRECTORY_SEPARATOR . $fileName); $this->assertStringStartsWith($prefix, $migrationContents); $this->assertStringContainsString('public function change()', $migrationContents); @@ -552,7 +552,7 @@ public function testCreateMigrationWithUpDownStyleAsFlag(): void $this->assertMatchesRegularExpression('/^[0-9]{14}.php/', $fileName); $date = substr($fileName, 0, 14); $this->assertFileExists($this->config->getMigrationPaths()[0]); - $prefix = "config->getMigrationPaths()[0] . DIRECTORY_SEPARATOR . $fileName); $this->assertStringStartsWith($prefix, $migrationContents); $this->assertStringContainsString('public function up()', $migrationContents); diff --git a/tests/Phinx/Console/Command/TemplateGenerators/DoesNotImplementRequiredInterface.php b/tests/Phinx/Console/Command/TemplateGenerators/DoesNotImplementRequiredInterface.php index 8816e9c3a..dfe660cc4 100644 --- a/tests/Phinx/Console/Command/TemplateGenerators/DoesNotImplementRequiredInterface.php +++ b/tests/Phinx/Console/Command/TemplateGenerators/DoesNotImplementRequiredInterface.php @@ -1,4 +1,5 @@ assertTrue($this->adapter->hasColumn('ntable', 'realname')); $this->assertTrue($this->adapter->hasColumn('ntable', 'email')); $this->assertFalse($this->adapter->hasColumn('ntable', 'address')); + + $columns = $this->adapter->getColumns('ntable'); + $this->assertCount(3, $columns); + $this->assertSame('id', $columns[0]->getName()); + $this->assertFalse($columns[0]->isSigned()); } public function testCreateTableWithComment() @@ -422,6 +428,25 @@ public function testCreateTableWithLatin1Collate() $this->assertEquals('latin1_general_ci', $row['Collation']); } + public function testCreateTableWithSignedPK() + { + $table = new \Phinx\Db\Table('ntable', ['signed' => true], $this->adapter); + $table->addColumn('realname', 'string') + ->addColumn('email', 'integer') + ->save(); + $this->assertTrue($this->adapter->hasTable('ntable')); + $this->assertTrue($this->adapter->hasColumn('ntable', 'id')); + $this->assertTrue($this->adapter->hasColumn('ntable', 'realname')); + $this->assertTrue($this->adapter->hasColumn('ntable', 'email')); + $this->assertFalse($this->adapter->hasColumn('ntable', 'address')); + $column_definitions = $this->adapter->getColumns('ntable'); + foreach ($column_definitions as $column_definition) { + if ($column_definition->getName() === 'id') { + $this->assertTrue($column_definition->getSigned()); + } + } + } + public function testCreateTableWithUnsignedPK() { $table = new \Phinx\Db\Table('ntable', ['signed' => false], $this->adapter); @@ -460,6 +485,24 @@ public function testCreateTableWithUnsignedNamedPK() $this->assertFalse($this->adapter->hasColumn('ntable', 'address')); } + /** + * @runInSeparateProcess + */ + public function testUnsignedPksFeatureFlag() + { + $this->adapter->connect(); + + FeatureFlags::$unsignedPrimaryKeys = false; + + $table = new \Phinx\Db\Table('table1', [], $this->adapter); + $table->create(); + + $columns = $this->adapter->getColumns('table1'); + $this->assertCount(1, $columns); + $this->assertSame('id', $columns[0]->getName()); + $this->assertTrue($columns[0]->getSigned()); + } + public function testCreateTableWithLimitPK() { $table = new \Phinx\Db\Table('ntable', ['id' => 'id', 'limit' => 4], $this->adapter); @@ -672,6 +715,53 @@ public function testAddColumnWithDefaultLiteral() $this->assertTrue($rows[1]['Default'] === 'CURRENT_TIMESTAMP' || $rows[1]['Default'] === 'current_timestamp()'); } + public function testAddColumnWithCustomType() + { + $this->adapter->setDataDomain([ + 'custom1' => [ + 'type' => 'enum', + 'null' => true, + 'values' => 'a,b,c', + ], + 'custom2' => [ + 'type' => 'enum', + 'null' => true, + 'values' => ['a', 'b', 'c'], + ], + ]); + + (new \Phinx\Db\Table('table1', [], $this->adapter)) + ->addColumn('custom1', 'custom1') + ->addColumn('custom2', 'custom2') + ->addColumn('custom_ext', 'custom2', [ + 'null' => false, + 'values' => ['d', 'e', 'f'], + ]) + ->save(); + + $this->assertTrue($this->adapter->hasTable('table1')); + + $columns = $this->adapter->getColumns('table1'); + + $this->assertArrayHasKey(1, $columns); + $this->assertArrayHasKey(2, $columns); + $this->assertArrayHasKey(3, $columns); + + foreach ([1, 2] as $index) { + $column = $this->adapter->getColumns('table1')[$index]; + $this->assertSame("custom{$index}", $column->getName()); + $this->assertSame('enum', $column->getType()); + $this->assertSame(['a', 'b', 'c'], $column->getValues()); + $this->assertTrue($column->getNull()); + } + + $column = $this->adapter->getColumns('table1')[3]; + $this->assertSame('custom_ext', $column->getName()); + $this->assertSame('enum', $column->getType()); + $this->assertSame(['d', 'e', 'f'], $column->getValues()); + $this->assertFalse($column->getNull()); + } + public function testAddColumnFirst() { $table = new \Phinx\Db\Table('table1', [], $this->adapter); @@ -972,7 +1062,7 @@ public function binaryToBlobAutomaticConversionData() } /** @dataProvider binaryToBlobAutomaticConversionData */ - public function testBinaryToBlobAutomaticConversion(?int $limit = null, string $expectedType, int $expectedLimit) + public function testBinaryToBlobAutomaticConversion(?int $limit, string $expectedType, int $expectedLimit) { $table = new \Phinx\Db\Table('t', [], $this->adapter); $table->addColumn('column1', 'binary', ['limit' => $limit]) @@ -999,7 +1089,7 @@ public function varbinaryToBlobAutomaticConversionData() } /** @dataProvider varbinaryToBlobAutomaticConversionData */ - public function testVarbinaryToBlobAutomaticConversion(?int $limit = null, string $expectedType, int $expectedLimit) + public function testVarbinaryToBlobAutomaticConversion(?int $limit, string $expectedType, int $expectedLimit) { $table = new \Phinx\Db\Table('t', [], $this->adapter); $table->addColumn('column1', 'varbinary', ['limit' => $limit]) @@ -1041,7 +1131,7 @@ public function blobColumnsData() } /** @dataProvider blobColumnsData */ - public function testblobColumns(string $type, string $expectedType, ?int $limit = null, int $expectedLimit) + public function testblobColumns(string $type, string $expectedType, ?int $limit, int $expectedLimit) { $table = new \Phinx\Db\Table('t', [], $this->adapter); $table->addColumn('column1', $type, ['limit' => $limit]) @@ -1200,7 +1290,7 @@ public function columnsProvider() ['column10', 'timestamp', []], ['column11', 'date', []], ['column12', 'binary', []], - ['column13', 'boolean', []], + ['column13', 'boolean', ['comment' => 'Lorem ipsum']], ['column14', 'string', ['limit' => 10]], ['column16', 'geometry', []], ['column17', 'point', []], @@ -1244,6 +1334,10 @@ public function testGetColumns($colName, $type, $options) if (isset($options['scale'])) { $this->assertEquals($options['scale'], $columns[1]->getScale()); } + + if (isset($options['comment'])) { + $this->assertEquals($options['comment'], $columns[1]->getComment()); + } } public function testGetColumnsInteger() diff --git a/tests/Phinx/Db/Adapter/PostgresAdapterTest.php b/tests/Phinx/Db/Adapter/PostgresAdapterTest.php index ac5de6280..b5d3c71bc 100644 --- a/tests/Phinx/Db/Adapter/PostgresAdapterTest.php +++ b/tests/Phinx/Db/Adapter/PostgresAdapterTest.php @@ -838,6 +838,39 @@ public function testAddColumnArrayType($column_name, $column_type) $this->assertTrue($table->hasColumn($column_name)); } + public function testAddColumnWithCustomType() + { + $this->adapter->setDataDomain([ + 'custom' => [ + 'type' => 'inet', + 'null' => true, + ], + ]); + + (new \Phinx\Db\Table('table1', [], $this->adapter)) + ->addColumn('custom', 'custom') + ->addColumn('custom_ext', 'custom', [ + 'null' => false, + ]) + ->save(); + + $this->assertTrue($this->adapter->hasTable('table1')); + + $columns = $this->adapter->getColumns('table1'); + $this->assertArrayHasKey(1, $columns); + $this->assertArrayHasKey(2, $columns); + + $column = $this->adapter->getColumns('table1')[1]; + $this->assertSame('custom', $column->getName()); + $this->assertSame('inet', $column->getType()); + $this->assertTrue($column->getNull()); + + $column = $this->adapter->getColumns('table1')[2]; + $this->assertSame('custom_ext', $column->getName()); + $this->assertSame('inet', $column->getType()); + $this->assertFalse($column->getNull()); + } + public function testRenameColumn() { $table = new \Phinx\Db\Table('t', [], $this->adapter); diff --git a/tests/Phinx/Db/Adapter/ProxyAdapterTest.php b/tests/Phinx/Db/Adapter/ProxyAdapterTest.php index 377538f53..79799a333 100644 --- a/tests/Phinx/Db/Adapter/ProxyAdapterTest.php +++ b/tests/Phinx/Db/Adapter/ProxyAdapterTest.php @@ -3,6 +3,7 @@ namespace Test\Phinx\Db\Adapter; use Phinx\Db\Adapter\ProxyAdapter; +use Phinx\Db\Table\Column; use Phinx\Migration\IrreversibleMigrationException; use PHPUnit\Framework\TestCase; @@ -62,6 +63,18 @@ public function testProxyAdapterCanInvertAddColumn() ->expects($this->any()) ->method('hasTable') ->will($this->returnValue(true)); + + $this->adapter + ->getAdapter() + ->expects($this->any()) + ->method('getColumnForType') + ->willReturnCallback(function (string $columnName, string $type, array $options) { + return (new Column()) + ->setName($columnName) + ->setType($type) + ->setOptions($options); + }); + $table = new \Phinx\Db\Table('atable', [], $this->adapter); $table->addColumn('acolumn', 'string') ->save(); diff --git a/tests/Phinx/Db/Adapter/SQLiteAdapterTest.php b/tests/Phinx/Db/Adapter/SQLiteAdapterTest.php index ce37686f7..80c0a5ee9 100644 --- a/tests/Phinx/Db/Adapter/SQLiteAdapterTest.php +++ b/tests/Phinx/Db/Adapter/SQLiteAdapterTest.php @@ -7,6 +7,7 @@ use InvalidArgumentException; use Phinx\Db\Adapter\SQLiteAdapter; use Phinx\Db\Table\Column; +use Phinx\Db\Table\ForeignKey; use Phinx\Util\Expression; use Phinx\Util\Literal; use Symfony\Component\Console\Input\ArrayInput; @@ -489,6 +490,43 @@ public function testAddColumnWithDefaultEmptyString() $this->assertEquals("''", $rows[1]['dflt_value']); } + public function testAddColumnWithCustomType() + { + $this->adapter->setDataDomain([ + 'custom' => [ + 'type' => 'string', + 'null' => true, + 'limit' => 15, + ], + ]); + + (new \Phinx\Db\Table('table1', [], $this->adapter)) + ->addColumn('custom', 'custom') + ->addColumn('custom_ext', 'custom', [ + 'null' => false, + 'limit' => 30, + ]) + ->save(); + + $this->assertTrue($this->adapter->hasTable('table1')); + + $columns = $this->adapter->getColumns('table1'); + $this->assertArrayHasKey(1, $columns); + $this->assertArrayHasKey(2, $columns); + + $column = $this->adapter->getColumns('table1')[1]; + $this->assertSame('custom', $column->getName()); + $this->assertSame('string', $column->getType()); + $this->assertSame(15, $column->getLimit()); + $this->assertTrue($column->getNull()); + + $column = $this->adapter->getColumns('table1')[2]; + $this->assertSame('custom_ext', $column->getName()); + $this->assertSame('string', $column->getType()); + $this->assertSame(30, $column->getLimit()); + $this->assertFalse($column->getNull()); + } + public function irregularCreateTableProvider() { return [ @@ -548,6 +586,332 @@ public function testRenamingANonExistentColumn() $this->adapter->renameColumn('t', 'column2', 'column1'); } + public function testRenameColumnWithIndex() + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol', 'integer') + ->addIndex('indexcol') + ->create(); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'indexcol')); + $this->assertFalse($this->adapter->hasIndex($table->getName(), 'newindexcol')); + + $table->renameColumn('indexcol', 'newindexcol')->update(); + + $this->assertFalse($this->adapter->hasIndex($table->getName(), 'indexcol')); + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'newindexcol')); + } + + public function testRenameColumnWithUniqueIndex() + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol', 'integer') + ->addIndex('indexcol', ['unique' => true]) + ->create(); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'indexcol')); + $this->assertFalse($this->adapter->hasIndex($table->getName(), 'newindexcol')); + + $table->renameColumn('indexcol', 'newindexcol')->update(); + + $this->assertFalse($this->adapter->hasIndex($table->getName(), 'indexcol')); + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'newindexcol')); + } + + public function testRenameColumnWithCompositeIndex() + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol1', 'integer') + ->addColumn('indexcol2', 'integer') + ->addIndex(['indexcol1', 'indexcol2']) + ->create(); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), ['indexcol1', 'indexcol2'])); + $this->assertFalse($this->adapter->hasIndex($table->getName(), ['indexcol1', 'newindexcol2'])); + + $table->renameColumn('indexcol2', 'newindexcol2')->update(); + + $this->assertFalse($this->adapter->hasIndex($table->getName(), ['indexcol1', 'indexcol2'])); + $this->assertTrue($this->adapter->hasIndex($table->getName(), ['indexcol1', 'newindexcol2'])); + } + + /** + * Tests that rewriting the index SQL does not accidentally change + * the table name in case it matches the column name. + */ + public function testRenameColumnWithIndexMatchingTheTableName() + { + $table = new \Phinx\Db\Table('indexcol', [], $this->adapter); + $table + ->addColumn('indexcol', 'integer') + ->addIndex('indexcol') + ->create(); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'indexcol')); + $this->assertFalse($this->adapter->hasIndex($table->getName(), 'newindexcol')); + + $table->renameColumn('indexcol', 'newindexcol')->update(); + + $this->assertFalse($this->adapter->hasIndex($table->getName(), 'indexcol')); + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'newindexcol')); + } + + /** + * Tests that rewriting the index SQL does not accidentally change + * column names that partially match the column to rename. + */ + public function testRenameColumnWithIndexColumnPartialMatch() + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol', 'integer') + ->addColumn('indexcolumn', 'integer') + ->create(); + + $this->adapter->execute('CREATE INDEX custom_idx ON t (indexcolumn, indexcol)'); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), ['indexcolumn', 'indexcol'])); + $this->assertFalse($this->adapter->hasIndex($table->getName(), ['indexcolumn', 'newindexcol'])); + + $table->renameColumn('indexcol', 'newindexcol')->update(); + + $this->assertFalse($this->adapter->hasIndex($table->getName(), ['indexcolumn', 'indexcol'])); + $this->assertTrue($this->adapter->hasIndex($table->getName(), ['indexcolumn', 'newindexcol'])); + } + + public function testRenameColumnWithIndexColumnRequiringQuoting() + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol', 'integer') + ->addIndex('indexcol') + ->create(); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'indexcol')); + $this->assertFalse($this->adapter->hasIndex($table->getName(), 'new index col')); + + $table->renameColumn('indexcol', 'new index col')->update(); + + $this->assertFalse($this->adapter->hasIndex($table->getName(), 'indexcol')); + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'new index col')); + } + + /** + * Indices that are using expressions are not being updated. + */ + public function testRenameColumnWithExpressionIndex() + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol', 'integer') + ->create(); + + $this->adapter->execute('CREATE INDEX custom_idx ON t (`indexcol`, ABS(`indexcol`))'); + + $this->assertTrue($this->adapter->hasIndexByName('t', 'custom_idx')); + + $this->expectException(\PDOException::class); + $this->expectExceptionMessage('no such column: indexcol'); + + $table->renameColumn('indexcol', 'newindexcol')->update(); + } + + /** + * Index SQL is mostly returned as-is, hence custom indices can contain + * a wide variety of formats. + */ + public function customIndexSQLDataProvider(): array + { + return [ + [ + 'CREATE INDEX test_idx ON t(indexcol);', + 'CREATE INDEX test_idx ON t(`newindexcol`)', + ], + [ + 'CREATE INDEX test_idx ON t(`indexcol`);', + 'CREATE INDEX test_idx ON t(`newindexcol`)', + ], + [ + 'CREATE INDEX test_idx ON t("indexcol");', + 'CREATE INDEX test_idx ON t(`newindexcol`)', + ], + [ + 'CREATE INDEX test_idx ON t([indexcol]);', + 'CREATE INDEX test_idx ON t(`newindexcol`)', + ], + [ + 'CREATE INDEX test_idx ON t(indexcol ASC);', + 'CREATE INDEX test_idx ON t(`newindexcol` ASC)', + ], + [ + 'CREATE INDEX test_idx ON t(`indexcol` ASC);', + 'CREATE INDEX test_idx ON t(`newindexcol` ASC)', + ], + [ + 'CREATE INDEX test_idx ON t("indexcol" DESC);', + 'CREATE INDEX test_idx ON t(`newindexcol` DESC)', + ], + [ + 'CREATE INDEX test_idx ON t([indexcol] DESC);', + 'CREATE INDEX test_idx ON t(`newindexcol` DESC)', + ], + [ + 'CREATE INDEX test_idx ON t(indexcol COLLATE BINARY);', + 'CREATE INDEX test_idx ON t(`newindexcol` COLLATE BINARY)', + ], + [ + 'CREATE INDEX test_idx ON t(indexcol COLLATE BINARY ASC);', + 'CREATE INDEX test_idx ON t(`newindexcol` COLLATE BINARY ASC)', + ], + [ + ' + cReATE uniQUE inDEx + iF nOT ExISts + main.test_idx on t ( + ( (( + inDEXcoL + ) )) COLLATE BINARY ASC + ); + ', + 'CREATE UNIQUE INDEX test_idx on t ( + ( (( + `newindexcol` + ) )) COLLATE BINARY ASC + )', + ], + ]; + } + + /** + * @dataProvider customIndexSQLDataProvider + * @param string $indexSQL Index creation SQL + * @param string $newIndexSQL Expected new index creation SQL + */ + public function testRenameColumnWithCustomIndex(string $indexSQL, string $newIndexSQL) + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol', 'integer') + ->create(); + + $this->adapter->execute($indexSQL); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'indexcol')); + $this->assertFalse($this->adapter->hasIndex($table->getName(), 'newindexcol')); + + $table->renameColumn('indexcol', 'newindexcol')->update(); + + $this->assertFalse($this->adapter->hasIndex($table->getName(), 'indexcol')); + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'newindexcol')); + + $index = $this->adapter->fetchRow("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = 'test_idx'"); + $this->assertSame($newIndexSQL, $index['sql']); + } + + /** + * Index SQL is mostly returned as-is, hence custom indices can contain + * a wide variety of formats. + */ + public function customCompositeIndexSQLDataProvider(): array + { + return [ + [ + 'CREATE INDEX test_idx ON t(indexcol1, indexcol2, indexcol3);', + 'CREATE INDEX test_idx ON t(indexcol1, `newindexcol`, indexcol3)', + ], + [ + 'CREATE INDEX test_idx ON t(`indexcol1`, `indexcol2`, `indexcol3`);', + 'CREATE INDEX test_idx ON t(`indexcol1`, `newindexcol`, `indexcol3`)', + ], + [ + 'CREATE INDEX test_idx ON t("indexcol1", "indexcol2", "indexcol3");', + 'CREATE INDEX test_idx ON t("indexcol1", `newindexcol`, "indexcol3")', + ], + [ + 'CREATE INDEX test_idx ON t([indexcol1], [indexcol2], [indexcol3]);', + 'CREATE INDEX test_idx ON t([indexcol1], `newindexcol`, [indexcol3])', + ], + [ + 'CREATE INDEX test_idx ON t(indexcol1 ASC, indexcol2 DESC, indexcol3);', + 'CREATE INDEX test_idx ON t(indexcol1 ASC, `newindexcol` DESC, indexcol3)', + ], + [ + 'CREATE INDEX test_idx ON t(`indexcol1` ASC, `indexcol2` DESC, `indexcol3`);', + 'CREATE INDEX test_idx ON t(`indexcol1` ASC, `newindexcol` DESC, `indexcol3`)', + ], + [ + 'CREATE INDEX test_idx ON t("indexcol1" ASC, "indexcol2" DESC, "indexcol3");', + 'CREATE INDEX test_idx ON t("indexcol1" ASC, `newindexcol` DESC, "indexcol3")', + ], + [ + 'CREATE INDEX test_idx ON t([indexcol1] ASC, [indexcol2] DESC, [indexcol3]);', + 'CREATE INDEX test_idx ON t([indexcol1] ASC, `newindexcol` DESC, [indexcol3])', + ], + [ + 'CREATE INDEX test_idx ON t(indexcol1 COLLATE BINARY, indexcol2 COLLATE NOCASE, indexcol3);', + 'CREATE INDEX test_idx ON t(indexcol1 COLLATE BINARY, `newindexcol` COLLATE NOCASE, indexcol3)', + ], + [ + 'CREATE INDEX test_idx ON t(indexcol1 COLLATE BINARY ASC, indexcol2 COLLATE NOCASE DESC, indexcol3);', + 'CREATE INDEX test_idx ON t(indexcol1 COLLATE BINARY ASC, `newindexcol` COLLATE NOCASE DESC, indexcol3)', + ], + [ + ' + cReATE uniQUE inDEx + iF nOT ExISts + main.test_idx on t ( + inDEXcoL1 , + ( (( + inDEXcoL2 + ) )) COLLATE BINARY ASC , + inDEXcoL3 + ); + ', + 'CREATE UNIQUE INDEX test_idx on t ( + inDEXcoL1 , + ( (( + `newindexcol` + ) )) COLLATE BINARY ASC , + inDEXcoL3 + )', + ], + ]; + } + + /** + * Index SQL is mostly returned as-is, hence custom indices can contain + * a wide variety of formats. + * + * @dataProvider customCompositeIndexSQLDataProvider + * @param string $indexSQL Index creation SQL + * @param string $newIndexSQL Expected new index creation SQL + */ + public function testRenameColumnWithCustomCompositeIndex(string $indexSQL, string $newIndexSQL) + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol1', 'integer') + ->addColumn('indexcol2', 'integer') + ->addColumn('indexcol3', 'integer') + ->create(); + + $this->adapter->execute($indexSQL); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), ['indexcol1', 'indexcol2', 'indexcol3'])); + $this->assertFalse($this->adapter->hasIndex($table->getName(), ['indexcol1', 'newindexcol', 'indexcol3'])); + + $table->renameColumn('indexcol2', 'newindexcol')->update(); + + $this->assertFalse($this->adapter->hasIndex($table->getName(), ['indexcol1', 'indexcol2', 'indexcol3'])); + $this->assertTrue($this->adapter->hasIndex($table->getName(), ['indexcol1', 'newindexcol', 'indexcol3'])); + + $index = $this->adapter->fetchRow("SELECT sql FROM sqlite_master WHERE type = 'index' AND name = 'test_idx'"); + $this->assertSame($newIndexSQL, $index['sql']); + } + public function testChangeColumn() { $table = new \Phinx\Db\Table('t', [], $this->adapter); @@ -710,6 +1074,157 @@ public function testDropColumn($columnCreationArgs) $this->assertFalse($this->adapter->hasColumn('t', $columnName)); } + public function testDropColumnWithIndex() + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol', 'integer') + ->addIndex('indexcol') + ->create(); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'indexcol')); + + $table->removeColumn('indexcol')->update(); + + $this->assertFalse($this->adapter->hasIndex($table->getName(), 'indexcol')); + } + + public function testDropColumnWithUniqueIndex() + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol', 'integer') + ->addIndex('indexcol', ['unique' => true]) + ->create(); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'indexcol')); + + $table->removeColumn('indexcol')->update(); + + $this->assertFalse($this->adapter->hasIndex($table->getName(), 'indexcol')); + } + + public function testDropColumnWithCompositeIndex() + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol1', 'integer') + ->addColumn('indexcol2', 'integer') + ->addIndex(['indexcol1', 'indexcol2']) + ->create(); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), ['indexcol1', 'indexcol2'])); + + $table->removeColumn('indexcol2')->update(); + + $this->assertFalse($this->adapter->hasIndex($table->getName(), ['indexcol1', 'indexcol2'])); + } + + /** + * Tests that removing columns does not accidentally drop indices + * on table names that match the column to remove. + */ + public function testDropColumnWithIndexMatchingTheTableName() + { + $table = new \Phinx\Db\Table('indexcol', [], $this->adapter); + $table + ->addColumn('indexcol', 'integer') + ->addColumn('indexcolumn', 'integer') + ->addIndex('indexcolumn') + ->create(); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'indexcolumn')); + + $table->removeColumn('indexcol')->update(); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'indexcolumn')); + } + + /** + * Tests that removing columns does not accidentally drop indices + * that contain column names that partially match the column to remove. + */ + public function testDropColumnWithIndexColumnPartialMatch() + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol', 'integer') + ->addColumn('indexcolumn', 'integer') + ->create(); + + $this->adapter->execute('CREATE INDEX custom_idx ON t (indexcolumn)'); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'indexcolumn')); + + $table->removeColumn('indexcol')->update(); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'indexcolumn')); + } + + /** + * Indices with expressions are not being removed. + */ + public function testDropColumnWithExpressionIndex() + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol', 'integer') + ->create(); + + $this->adapter->execute('CREATE INDEX custom_idx ON t (ABS(indexcol))'); + + $this->assertTrue($this->adapter->hasIndexByName('t', 'custom_idx')); + + $this->expectException(\PDOException::class); + $this->expectExceptionMessage('no such column: indexcol'); + + $table->removeColumn('indexcol')->update(); + } + + /** + * @dataProvider customIndexSQLDataProvider + * @param string $indexSQL Index creation SQL + */ + public function testDropColumnWithCustomIndex(string $indexSQL) + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol', 'integer') + ->create(); + + $this->adapter->execute($indexSQL); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), 'indexcol')); + + $table->removeColumn('indexcol')->update(); + + $this->assertFalse($this->adapter->hasIndex($table->getName(), 'indexcol')); + } + + /** + * @dataProvider customCompositeIndexSQLDataProvider + * @param string $indexSQL Index creation SQL + */ + public function testDropColumnWithCustomCompositeIndex(string $indexSQL) + { + $table = new \Phinx\Db\Table('t', [], $this->adapter); + $table + ->addColumn('indexcol1', 'integer') + ->addColumn('indexcol2', 'integer') + ->addColumn('indexcol3', 'integer') + ->create(); + + $this->adapter->execute($indexSQL); + + $this->assertTrue($this->adapter->hasIndex($table->getName(), ['indexcol1', 'indexcol2', 'indexcol3'])); + $this->assertFalse($this->adapter->hasIndex($table->getName(), ['indexcol1', 'indexcol3'])); + + $table->removeColumn('indexcol2')->update(); + + $this->assertFalse($this->adapter->hasIndex($table->getName(), ['indexcol1', 'indexcol2', 'indexcol3'])); + $this->assertFalse($this->adapter->hasIndex($table->getName(), ['indexcol1', 'indexcol3'])); + } + public function columnCreationArgumentProvider() { return [ @@ -1372,6 +1887,209 @@ public function testAlterTableWithConstraints() } } + /** + * Tests that operations that trigger implicit table drops will not cause + * a foreign key constraint violation error. + */ + public function testAlterTableDoesNotViolateRestrictedForeignKeyConstraint() + { + $this->adapter->execute('PRAGMA foreign_keys = ON'); + + $articlesTable = new \Phinx\Db\Table('articles', [], $this->adapter); + $articlesTable + ->insert(['id' => 1]) + ->save(); + + $commentsTable = new \Phinx\Db\Table('comments', [], $this->adapter); + $commentsTable + ->addColumn('article_id', 'integer') + ->addForeignKey('article_id', 'articles', 'id', [ + 'update' => ForeignKey::RESTRICT, + 'delete' => ForeignKey::RESTRICT, + ]) + ->insert(['id' => 1, 'article_id' => 1]) + ->save(); + + $this->assertTrue($this->adapter->hasForeignKey('comments', ['article_id'])); + + $articlesTable + ->addColumn('new_column', 'integer') + ->update(); + + $articlesTable + ->renameColumn('new_column', 'new_column_renamed') + ->update(); + + $articlesTable + ->changeColumn('new_column_renamed', 'integer', [ + 'default' => 1, + ]) + ->update(); + + $articlesTable + ->removeColumn('new_column_renamed') + ->update(); + + $articlesTable + ->addIndex('id', ['name' => 'ID_IDX']) + ->update(); + + $articlesTable + ->removeIndex('id') + ->update(); + + $articlesTable + ->addForeignKey('id', 'comments', 'id') + ->update(); + + $articlesTable + ->dropForeignKey('id') + ->update(); + + $articlesTable + ->addColumn('id2', 'integer') + ->addIndex('id', ['unique' => true]) + ->changePrimaryKey('id2') + ->update(); + } + + /** + * Tests that foreign key constraint violations introduced around the table + * alteration process (being it implicitly by the process itself or by the user) + * will trigger an error accordingly. + */ + public function testAlterTableDoesViolateForeignKeyConstraintOnTargetTableChange() + { + $articlesTable = new \Phinx\Db\Table('articles', [], $this->adapter); + $articlesTable + ->insert(['id' => 1]) + ->save(); + + $commentsTable = new \Phinx\Db\Table('comments', [], $this->adapter); + $commentsTable + ->addColumn('article_id', 'integer') + ->addForeignKey('article_id', 'articles', 'id', [ + 'update' => ForeignKey::RESTRICT, + 'delete' => ForeignKey::RESTRICT, + ]) + ->insert(['id' => 1, 'article_id' => 1]) + ->save(); + + $this->assertTrue($this->adapter->hasForeignKey('comments', ['article_id'])); + + $this->adapter->execute('PRAGMA foreign_keys = OFF'); + $this->adapter->execute('DELETE FROM articles'); + $this->adapter->execute('PRAGMA foreign_keys = ON'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Integrity constraint violation: FOREIGN KEY constraint on `comments` failed.'); + + $articlesTable + ->addColumn('new_column', 'integer') + ->update(); + } + + /** + * Tests that foreign key constraint violations introduced around the table + * alteration process (being it implicitly by the process itself or by the user) + * will trigger an error accordingly. + */ + public function testAlterTableDoesViolateForeignKeyConstraintOnSourceTableChange() + { + $adapter = $this + ->getMockBuilder(SQLiteAdapter::class) + ->setConstructorArgs([SQLITE_DB_CONFIG, new ArrayInput([]), new NullOutput()]) + ->onlyMethods(['query']) + ->getMock(); + + $adapterReflection = new \ReflectionObject($adapter); + $queryReflection = $adapterReflection->getParentClass()->getMethod('query'); + + $adapter + ->expects($this->atLeastOnce()) + ->method('query') + ->willReturnCallback(function (string $sql, array $params = []) use ($adapter, $queryReflection) { + if ($sql === 'PRAGMA foreign_key_check(`comments`)') { + $adapter->execute('PRAGMA foreign_keys = OFF'); + $adapter->execute('DELETE FROM articles'); + $adapter->execute('PRAGMA foreign_keys = ON'); + } + + return $queryReflection->invoke($adapter, $sql, $params); + }); + + $articlesTable = new \Phinx\Db\Table('articles', [], $adapter); + $articlesTable + ->insert(['id' => 1]) + ->save(); + + $commentsTable = new \Phinx\Db\Table('comments', [], $adapter); + $commentsTable + ->addColumn('article_id', 'integer') + ->addForeignKey('article_id', 'articles', 'id', [ + 'update' => ForeignKey::RESTRICT, + 'delete' => ForeignKey::RESTRICT, + ]) + ->insert(['id' => 1, 'article_id' => 1]) + ->save(); + + $this->assertTrue($adapter->hasForeignKey('comments', ['article_id'])); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Integrity constraint violation: FOREIGN KEY constraint on `comments` failed.'); + + $commentsTable + ->addColumn('new_column', 'integer') + ->update(); + } + + /** + * Tests that the adapter's foreign key validation does not apply when + * the `foreign_keys` pragma is set to `OFF`. + */ + public function testAlterTableForeignKeyConstraintValidationNotRunningWithDisabledForeignKeys() + { + $articlesTable = new \Phinx\Db\Table('articles', [], $this->adapter); + $articlesTable + ->insert(['id' => 1]) + ->save(); + + $commentsTable = new \Phinx\Db\Table('comments', [], $this->adapter); + $commentsTable + ->addColumn('article_id', 'integer') + ->addForeignKey('article_id', 'articles', 'id', [ + 'update' => ForeignKey::RESTRICT, + 'delete' => ForeignKey::RESTRICT, + ]) + ->insert(['id' => 1, 'article_id' => 1]) + ->save(); + + $this->assertTrue($this->adapter->hasForeignKey('comments', ['article_id'])); + + $this->adapter->execute('PRAGMA foreign_keys = OFF'); + $this->adapter->execute('DELETE FROM articles'); + + $noException = false; + try { + $articlesTable + ->addColumn('new_column1', 'integer') + ->update(); + + $noException = true; + } finally { + $this->assertTrue($noException); + } + + $this->adapter->execute('PRAGMA foreign_keys = ON'); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Integrity constraint violation: FOREIGN KEY constraint on `comments` failed.'); + + $articlesTable + ->addColumn('new_column2', 'integer') + ->update(); + } + public function testLiteralSupport() { $createQuery = <<<'INPUT' @@ -2330,7 +3048,10 @@ public function testForeignKeyReferenceCorrectAfterChangePrimaryKey() $table->addForeignKey($refTableColumnId, $refTable->getName(), 'id'); $table->save(); - $refTable->changePrimaryKey($refTableColumnAdditionalId)->save(); + $refTable + ->addIndex('id', ['unique' => true]) + ->changePrimaryKey($refTableColumnAdditionalId) + ->save(); $this->assertTrue($this->adapter->hasForeignKey($table->getName(), [$refTableColumnId])); $this->assertFalse($this->adapter->hasTable("tmp_{$refTable->getName()}")); diff --git a/tests/Phinx/Db/Adapter/SqlServerAdapterTest.php b/tests/Phinx/Db/Adapter/SqlServerAdapterTest.php index 37f15a985..e2ee43aac 100644 --- a/tests/Phinx/Db/Adapter/SqlServerAdapterTest.php +++ b/tests/Phinx/Db/Adapter/SqlServerAdapterTest.php @@ -461,6 +461,39 @@ public function testAddColumnWithDefaultBool() } } + public function testAddColumnWithCustomType() + { + $this->adapter->setDataDomain([ + 'custom' => [ + 'type' => 'geometry', + 'null' => true, + ], + ]); + + (new \Phinx\Db\Table('table1', [], $this->adapter)) + ->addColumn('custom', 'custom') + ->addColumn('custom_ext', 'custom', [ + 'null' => false, + ]) + ->save(); + + $this->assertTrue($this->adapter->hasTable('table1')); + + $columns = $this->adapter->getColumns('table1'); + $this->assertArrayHasKey('custom', $columns); + $this->assertArrayHasKey('custom_ext', $columns); + + $column = $this->adapter->getColumns('table1')['custom']; + $this->assertSame('custom', $column->getName()); + $this->assertSame('geometry', (string)$column->getType()); + $this->assertTrue($column->getNull()); + + $column = $this->adapter->getColumns('table1')['custom_ext']; + $this->assertSame('custom_ext', $column->getName()); + $this->assertSame('geometry', (string)$column->getType()); + $this->assertFalse($column->getNull()); + } + public function testRenameColumn() { $table = new \Phinx\Db\Table('t', [], $this->adapter); diff --git a/tests/Phinx/Db/Table/ColumnTest.php b/tests/Phinx/Db/Table/ColumnTest.php index 40bf654b5..d2000ce0d 100644 --- a/tests/Phinx/Db/Table/ColumnTest.php +++ b/tests/Phinx/Db/Table/ColumnTest.php @@ -2,6 +2,7 @@ namespace Test\Phinx\Db\Table; +use Phinx\Config\FeatureFlags; use Phinx\Db\Table\Column; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -28,4 +29,17 @@ public function testSetOptionsIdentity() $this->assertFalse($column->isNull()); $this->assertTrue($column->isIdentity()); } + + /** + * @runInSeparateProcess + */ + public function testColumnNullFeatureFlag() + { + $column = new Column(); + $this->assertTrue($column->isNull()); + + FeatureFlags::$columnNullDefault = false; + $column = new Column(); + $this->assertFalse($column->isNull()); + } } diff --git a/tests/Phinx/Migration/ManagerTest.php b/tests/Phinx/Migration/ManagerTest.php index 02101fb61..1464369fd 100644 --- a/tests/Phinx/Migration/ManagerTest.php +++ b/tests/Phinx/Migration/ManagerTest.php @@ -6132,4 +6132,41 @@ public function testMigrationWillNotBeExecuted() $this->assertTrue($adapter->hasTable('info')); } + + public function testMigrationWithCustomColumnTypes() + { + $adapter = $this->prepareEnvironment([ + 'migrations' => $this->getCorrectedPath(__DIR__ . '/_files/custom_column_types'), + ]); + + $this->manager->migrate('production'); + + $this->assertTrue($adapter->hasTable('users')); + + $columns = array_values($adapter->getColumns('users')); + $this->assertArrayHasKey(3, $columns); + $this->assertArrayHasKey(4, $columns); + + $limit = 15; + if ($adapter->getAdapterType() === 'pgsql') { + $limit = null; + } + + $column = $columns[3]; + $this->assertSame('phone_number', $column->getName()); + $this->assertSame('string', $column->getType()); + $this->assertSame($limit, $column->getLimit()); + $this->assertTrue($column->getNull()); + + $limit = 30; + if ($adapter->getAdapterType() === 'pgsql') { + $limit = null; + } + + $column = $columns[4]; + $this->assertSame('phone_number_ext', $column->getName()); + $this->assertSame('string', $column->getType()); + $this->assertSame($limit, $column->getLimit()); + $this->assertFalse($column->getNull()); + } } diff --git a/tests/Phinx/Migration/_files/custom_column_types/20221214154118_add_column_with_custom_type.php b/tests/Phinx/Migration/_files/custom_column_types/20221214154118_add_column_with_custom_type.php new file mode 100644 index 000000000..18ed5654c --- /dev/null +++ b/tests/Phinx/Migration/_files/custom_column_types/20221214154118_add_column_with_custom_type.php @@ -0,0 +1,23 @@ +table('users') + ->addColumn('first_name', Column::STRING, ['limit' => 30]) + ->addColumn('last_name', Column::STRING, ['limit' => 30]) + ->addColumn('phone_number', 'phone_number') + ->addColumn('phone_number_ext', 'phone_number', [ + 'null' => false, + 'limit' => 30, + ]) + ->create(); + } +} diff --git a/tests/Phinx/TestCase.php b/tests/Phinx/TestCase.php index e86d2e522..a9c047c75 100644 --- a/tests/Phinx/TestCase.php +++ b/tests/Phinx/TestCase.php @@ -1,4 +1,5 @@