diff --git a/src/db/ColumnSchema.php b/src/db/ColumnSchema.php index 71127a24..90b43f06 100644 --- a/src/db/ColumnSchema.php +++ b/src/db/ColumnSchema.php @@ -25,4 +25,24 @@ class ColumnSchema extends \yii\db\ColumnSchema * ``` */ public $xDbType; + + /** + * Used only for MySQL/MariaDB + * @var array|null + * [ + * index => int # position: starts from 1 + * after => ?string # after column + * before => ?string # before column + * ] + * If `before` is null then column is last + * If `after` is null then column is first + * If both are null then table has only 1 column + */ + public ?array $fromPosition = null; + public ?array $toPosition = null; + + /** + * From `$this->fromPosition` and `$this->toPosition` we can check if the position is changed or not. This is done in `BaseMigrationBuilder::setColumnsPositions()` + */ + public bool $isPositionChanged = false; } diff --git a/src/generator/default/dbmodel.php b/src/generator/default/dbmodel.php index 17fa4a84..a3d269b6 100644 --- a/src/generator/default/dbmodel.php +++ b/src/generator/default/dbmodel.php @@ -49,8 +49,8 @@ */ abstract class getClassName() ?> extends \yii\db\ActiveRecord { -getScenarios()): -foreach($scenarios as $scenario): ?> +getScenarios()): +foreach ($scenarios as $scenario): ?> /** * @@ -76,7 +76,7 @@ public static function tableName() { return getTableAlias()) ?>; } - + /** * Automatically generated scenarios from the model 'x-scenarios'. @@ -92,7 +92,7 @@ public function scenarios() $default = parent::scenarios()[self::SCENARIO_DEFAULT]; return [ - + self:: => $default, /** diff --git a/src/lib/migrations/BaseMigrationBuilder.php b/src/lib/migrations/BaseMigrationBuilder.php index d4c49c21..1bd09e9b 100644 --- a/src/lib/migrations/BaseMigrationBuilder.php +++ b/src/lib/migrations/BaseMigrationBuilder.php @@ -171,6 +171,7 @@ public function buildSecondary(?ManyToManyRelation $relation = null):MigrationMo { $this->migration = Yii::createObject(MigrationModel::class, [$this->model, false, $relation, []]); $this->newColumns = $relation->columnSchema ?? $this->model->attributesToColumnSchema(); + $this->setColumnsPositions(); $wantNames = array_keys($this->newColumns); $haveNames = $this->tableSchema->columnNames; $columnsForCreate = array_map( @@ -184,7 +185,7 @@ function (string $missingColumn) { function (string $unknownColumn) { return $this->tableSchema->columns[$unknownColumn]; }, - array_diff($haveNames, $wantNames) + array_reverse(array_diff($haveNames, $wantNames), true) ); $columnsForChange = array_intersect($wantNames, $haveNames); @@ -220,6 +221,7 @@ function (string $unknownColumn) { } else { $this->buildRelations(); } + return $this->migration; } @@ -249,12 +251,12 @@ protected function buildColumnsDrop(array $columns):void { foreach ($columns as $column) { $tableName = $this->model->getTableAlias(); + $position = $this->findPosition($column, true); if ($column->isPrimaryKey && !$column->autoIncrement) { $pkName = 'pk_' . $this->model->tableName . '_' . $column->name; $this->migration->addDownCode($this->recordBuilder->addPrimaryKey($tableName, [$column->name], $pkName)) ->addUpCode($this->recordBuilder->dropPrimaryKey($tableName, [$column->name], $pkName)); } - $position = $this->findPosition($column, true); $this->migration->addDownCode($this->recordBuilder->addDbColumn($tableName, $column, $position)) ->addUpCode($this->recordBuilder->dropColumn($tableName, $column->name)); } @@ -513,47 +515,6 @@ public function isDefaultValueChanged( return false; } - /** - * Given a column, compute its previous column name present in OpenAPI schema - * @return ?string - * `null` if column is added at last - * 'FIRST' if column is added at first position - * 'AFTER ' if column is added in between e.g. if 'email' is added after 'username' then 'AFTER username' - */ - public function findPosition(ColumnSchema $column, bool $forDrop = false): ?string - { - $columnNames = array_keys($forDrop ? $this->tableSchema->columns : $this->newColumns); - - $key = array_search($column->name, $columnNames); - if ($key > 0) { - $prevColName = $columnNames[$key-1]; - - if (!isset($columnNames[$key+1])) { // if new col is added at last then no need to add 'AFTER' SQL part. This is checked as if next column is present or not - return null; - } - - // in case of `down()` code of migration, putting 'after ' in add column statmenet is erroneous because may not exist. - // Example: From col a, b, c, d, if I drop c and d then their migration code will be generated like: - // `up()` code - // drop c - // drop d - // `down()` code - // add d after c (c does not exist! Error!) - // add c - if ($forDrop) { - return null; - } - - - return self::POS_AFTER . ' ' . $prevColName; - - // if no `$columnSchema` is found, previous column does not exist. This happens when 'after column' is not yet added in migration or added after currently undertaken column - } elseif ($key === 0) { - return self::POS_FIRST; - } - - return null; - } public function modifyDesiredFromDbInContextOfDesired(ColumnSchema $desired, ColumnSchema $desiredFromDb): void { @@ -574,4 +535,19 @@ public function modifyDesiredInContextOfDesiredFromDb(ColumnSchema $desired, Col } $desired->dbType = $desiredFromDb->dbType; } + + /** + * Only for MySQL and MariaDB + * Given a column, compute its previous column name present in OpenAPI schema + * @param ColumnSchema $column + * @param bool $forDrop + * @param bool $forAlter + * @return ?string + * `null` if column is added at last + * 'FIRST' if column is added at first position + * 'AFTER ' if column is added in between e.g. if 'email' is added after 'username' then 'AFTER username' + */ + abstract public function findPosition(ColumnSchema $column, bool $forDrop = false, bool $forAlter = false): ?string; + + abstract public function setColumnsPositions(); } diff --git a/src/lib/migrations/MigrationRecordBuilder.php b/src/lib/migrations/MigrationRecordBuilder.php index 98ee03b8..73fdce7d 100644 --- a/src/lib/migrations/MigrationRecordBuilder.php +++ b/src/lib/migrations/MigrationRecordBuilder.php @@ -112,10 +112,10 @@ public function addDbColumn(string $tableAlias, ColumnSchema $column, ?string $p /** * @throws \yii\base\InvalidConfigException */ - public function alterColumn(string $tableAlias, ColumnSchema $column):string + public function alterColumn(string $tableAlias, ColumnSchema $column, ?string $position = null):string { if (property_exists($column, 'xDbType') && is_string($column->xDbType) && !empty($column->xDbType)) { - $converter = $this->columnToCode($tableAlias, $column, true, false, true, true); + $converter = $this->columnToCode($tableAlias, $column, true, false, true, true, $position); return sprintf( ApiGenerator::isPostgres() ? self::ALTER_COLUMN_RAW_PGSQL : self::ALTER_COLUMN_RAW, $tableAlias, @@ -123,17 +123,21 @@ public function alterColumn(string $tableAlias, ColumnSchema $column):string ColumnToCode::escapeQuotes($converter->getCode()) ); } - $converter = $this->columnToCode($tableAlias, $column, true); + $converter = $this->columnToCode($tableAlias, $column, true, false, false, false, $position); return sprintf(self::ALTER_COLUMN, $tableAlias, $column->name, $converter->getCode(true)); } /** * @throws \yii\base\InvalidConfigException */ - public function alterColumnType(string $tableAlias, ColumnSchema $column, bool $addUsing = false):string - { + public function alterColumnType( + string $tableAlias, + ColumnSchema $column, + bool $addUsing = false, + ?string $position = null + ):string { if (property_exists($column, 'xDbType') && is_string($column->xDbType) && !empty($column->xDbType)) { - $converter = $this->columnToCode($tableAlias, $column, false, false, true, true); + $converter = $this->columnToCode($tableAlias, $column, false, false, true, true, $position); return sprintf( ApiGenerator::isPostgres() ? self::ALTER_COLUMN_RAW_PGSQL : self::ALTER_COLUMN_RAW, $tableAlias, @@ -141,7 +145,7 @@ public function alterColumnType(string $tableAlias, ColumnSchema $column, bool $ rtrim(ltrim($converter->getAlterExpression($addUsing), "'"), "'") ); } - $converter = $this->columnToCode($tableAlias, $column, false); + $converter = $this->columnToCode($tableAlias, $column, false, false, false, false, $position); return sprintf(self::ALTER_COLUMN, $tableAlias, $column->name, $converter->getAlterExpression($addUsing)); } @@ -149,10 +153,14 @@ public function alterColumnType(string $tableAlias, ColumnSchema $column, bool $ * This method is only used in Pgsql * @throws \yii\base\InvalidConfigException */ - public function alterColumnTypeFromDb(string $tableAlias, ColumnSchema $column, bool $addUsing = false) :string - { + public function alterColumnTypeFromDb( + string $tableAlias, + ColumnSchema $column, + bool $addUsing = false, + ?string $position = null + ) :string { if (property_exists($column, 'xDbType') && is_string($column->xDbType) && !empty($column->xDbType)) { - $converter = $this->columnToCode($tableAlias, $column, true, false, true, true); + $converter = $this->columnToCode($tableAlias, $column, true, false, true, true, $position); return sprintf( ApiGenerator::isPostgres() ? self::ALTER_COLUMN_RAW_PGSQL : self::ALTER_COLUMN_RAW, $tableAlias, @@ -160,7 +168,7 @@ public function alterColumnTypeFromDb(string $tableAlias, ColumnSchema $column, rtrim(ltrim($converter->getAlterExpression($addUsing), "'"), "'") ); } - $converter = $this->columnToCode($tableAlias, $column, true); + $converter = $this->columnToCode($tableAlias, $column, true, false, false, false, $position); return sprintf(self::ALTER_COLUMN, $tableAlias, $column->name, $converter->getAlterExpression($addUsing)); } diff --git a/src/lib/migrations/MysqlMigrationBuilder.php b/src/lib/migrations/MysqlMigrationBuilder.php index 42354df3..b28040a0 100644 --- a/src/lib/migrations/MysqlMigrationBuilder.php +++ b/src/lib/migrations/MysqlMigrationBuilder.php @@ -14,9 +14,7 @@ use yii\db\ColumnSchema; use yii\db\IndexConstraint; use yii\db\Schema; -use \Yii; use yii\helpers\ArrayHelper; -use yii\helpers\VarDumper; final class MysqlMigrationBuilder extends BaseMigrationBuilder { @@ -25,6 +23,13 @@ final class MysqlMigrationBuilder extends BaseMigrationBuilder */ protected function buildColumnChanges(ColumnSchema $current, ColumnSchema $desired, array $changed):void { + $positionCurrent = $positionDesired = null; + if (in_array('position', $changed, true)) { + $positionDesired = $this->findPosition($desired, false, true); + $positionCurrent = $this->findPosition($desired, true, true); + $key = array_search('position', $changed, true); + unset($changed[$key]); + } $newColumn = clone $current; foreach ($changed as $attr) { $newColumn->$attr = $desired->$attr; @@ -32,8 +37,8 @@ protected function buildColumnChanges(ColumnSchema $current, ColumnSchema $desir if (static::isEnum($newColumn)) { $newColumn->dbType = 'enum'; // TODO this is concretely not correct } - $this->migration->addUpCode($this->recordBuilder->alterColumn($this->model->getTableAlias(), $newColumn)) - ->addDownCode($this->recordBuilder->alterColumn($this->model->getTableAlias(), $current)); + $this->migration->addUpCode($this->recordBuilder->alterColumn($this->model->getTableAlias(), $newColumn, $positionDesired)) + ->addDownCode($this->recordBuilder->alterColumn($this->model->getTableAlias(), $current, $positionCurrent)); } protected function compareColumns(ColumnSchema $current, ColumnSchema $desired):array @@ -68,6 +73,11 @@ protected function compareColumns(ColumnSchema $current, ColumnSchema $desired): } } } + + if (property_exists($desired, 'isPositionChanged') && $desired->isPositionChanged) { + $changedAttributes[] = 'position'; + } + return $changedAttributes; } @@ -159,4 +169,139 @@ public function modifyDesiredInContextOfCurrent(ColumnSchema $current, ColumnSch $desired->size = $current->size; } } + + /** + * {@inheritDoc} + */ + public function findPosition(ColumnSchema $column, bool $forDrop = false, bool $forAlter = false): ?string + { + $columnNames = array_keys($forDrop ? $this->tableSchema->columns : $this->newColumns); + + $key = array_search($column->name, $columnNames); + if ($key > 0) { + $prevColName = $columnNames[$key - 1]; + if (($key === count($columnNames) - 1) && !$forAlter) { + return null; + } + + if (array_key_exists($prevColName, $forDrop ? $this->tableSchema->columns : $this->newColumns)) { + if ($forDrop && !$forAlter) { + // if the previous column is the last one in the want names then no need for AFTER + $cols = array_keys($this->newColumns); + if ($prevColName === array_pop($cols)) { + return null; + } + } + if ($forAlter && $forDrop) { + if (!array_key_exists($prevColName, $this->newColumns)) { + return null; + } + } + return self::POS_AFTER . ' ' . $prevColName; + } + return null; + + // if no `$columnSchema` is found, previous column does not exist. This happens when 'after column' is not yet added in migration or added after currently undertaken column + } elseif ($key === 0) { + return self::POS_FIRST; + } + + return null; + } + + public function setColumnsPositions() + { + $i = 0; + $haveColumns = $this->tableSchema->columns; + $wantNames = array_keys($this->newColumns); + $haveNames = array_keys($haveColumns); + + // Part 1/2 compute from and to position + foreach ($this->newColumns as $name => $column) { + /** @var \cebe\yii2openapi\db\ColumnSchema $column */ + $column->toPosition = [ + 'index' => $i + 1, + 'after' => $i === 0 ? null : $wantNames[$i - 1], + 'before' => $i === (count($wantNames) - 1) ? null : $wantNames[$i + 1], + ]; + + if (isset($haveColumns[$name])) { + $index = array_search($name, $haveNames) + 1; + $column->fromPosition = [ + 'index' => $index, + 'after' => $haveNames[$index - 2] ?? null, + 'before' => $haveNames[$index] ?? null, + ]; + } + + $i++; + } + + // Part 2/2 compute is position is really changed + + // check if only new columns are added without any explicit position change + $namesForCreate = array_diff($wantNames, $haveNames); + $wantNamesWoNewCols = array_values(array_diff($wantNames, $namesForCreate)); + if ($namesForCreate && $haveNames === $wantNamesWoNewCols) { + return; + } + // check if only existing columns are deleted without any explicit position change + $namesForDrop = array_diff($haveNames, $wantNames); + $haveNamesWoDropCols = array_values(array_diff($haveNames, $namesForDrop)); + if ($namesForDrop && $wantNames === $haveNamesWoDropCols) { + return; + } + // check both above simultaneously + if ($namesForCreate && $namesForDrop && ($wantNamesWoNewCols === $haveNamesWoDropCols)) { + return; + } + + $takenIndices = $nonRedundantIndices = []; # $nonRedundantIndices are the wanted ones which are created by moving of one or more columns. Example: if a column is moved from 2nd to 8th position then we will consider only one column is moved ignoring index/position change(-1) of 4rd to 8th column (4->3, 5->4 ...). So migration for this unwanted indices changes won't be generated. `$takenIndices` might have redundant indices + foreach ($this->newColumns as $column) { + /** @var \cebe\yii2openapi\db\ColumnSchema $column */ + + if (!$column->fromPosition || !$column->toPosition) { + continue; + } + if (is_int(array_search([$column->toPosition['index'], $column->fromPosition['index']], $takenIndices))) { + continue; + } + if ($column->fromPosition === $column->toPosition) { + continue; + } + if ($column->fromPosition['index'] === $column->toPosition['index']) { + continue; + } + + $column->isPositionChanged = true; + $takenIndices[] = [$column->fromPosition['index'], $column->toPosition['index']]; + + // ------- + if (($column->fromPosition['before'] !== $column->toPosition['before']) && + ($column->fromPosition['after'] !== $column->toPosition['after']) + ) { + $nonRedundantIndices[] = [$column->fromPosition['index'], $column->toPosition['index']]; + } + } + + foreach ($this->newColumns as $column) { + /** @var \cebe\yii2openapi\db\ColumnSchema $column */ + + if (!isset($column->toPosition['index'], $column->fromPosition['index'])) { + continue; + } + $condition = (abs($column->toPosition['index'] - $column->fromPosition['index']) === count($nonRedundantIndices)); + if (($column->fromPosition['before'] === $column->toPosition['before']) + && $condition + ) { + $column->isPositionChanged = false; + continue; + } + if (($column->fromPosition['after'] === $column->toPosition['after']) + && $condition + ) { + $column->isPositionChanged = false; + } + } + } } diff --git a/src/lib/migrations/PostgresMigrationBuilder.php b/src/lib/migrations/PostgresMigrationBuilder.php index b8c9324d..82dcfc76 100644 --- a/src/lib/migrations/PostgresMigrationBuilder.php +++ b/src/lib/migrations/PostgresMigrationBuilder.php @@ -9,7 +9,6 @@ use cebe\yii2openapi\lib\items\DbIndex; use yii\db\ColumnSchema; -use yii\helpers\VarDumper; use yii\helpers\ArrayHelper; final class PostgresMigrationBuilder extends BaseMigrationBuilder @@ -248,4 +247,16 @@ public function modifyDesiredInContextOfCurrent(ColumnSchema $current, ColumnSch $desired->size = $current->size; } } + + /** + * {@inheritDoc} + */ + public function findPosition(ColumnSchema $column, bool $forDrop = false, bool $forAlter = false): ?string + { + return null; + } + + public function setColumnsPositions() + { + } } diff --git a/tests/specs/blog_v2/migrations_maria_db/m200000_000004_change_table_v2_users.php b/tests/specs/blog_v2/migrations_maria_db/m200000_000004_change_table_v2_users.php index bf1c82fd..1231f6db 100644 --- a/tests/specs/blog_v2/migrations_maria_db/m200000_000004_change_table_v2_users.php +++ b/tests/specs/blog_v2/migrations_maria_db/m200000_000004_change_table_v2_users.php @@ -25,7 +25,7 @@ public function down() $this->alterColumn('{{%v2_users}}', 'created_at', $this->timestamp()->null()->defaultExpression("current_timestamp()")); $this->alterColumn('{{%v2_users}}', 'role', $this->string(20)->null()->defaultValue('reader')); $this->alterColumn('{{%v2_users}}', 'email', $this->string(200)->notNull()); - $this->addColumn('{{%v2_users}}', 'username', $this->string(200)->notNull()); + $this->addColumn('{{%v2_users}}', 'username', $this->string(200)->notNull()->after('id')); $this->dropColumn('{{%v2_users}}', 'login'); } } diff --git a/tests/specs/blog_v2/migrations_maria_db/m200000_000005_change_table_v2_comments.php b/tests/specs/blog_v2/migrations_maria_db/m200000_000005_change_table_v2_comments.php index 75553729..ead3128c 100644 --- a/tests/specs/blog_v2/migrations_maria_db/m200000_000005_change_table_v2_comments.php +++ b/tests/specs/blog_v2/migrations_maria_db/m200000_000005_change_table_v2_comments.php @@ -25,7 +25,7 @@ public function down() $this->alterColumn('{{%v2_comments}}', 'created_at', $this->integer(11)->notNull()); $this->alterColumn('{{%v2_comments}}', 'meta_data', 'json NOT NULL DEFAULT \'[]\''); $this->alterColumn('{{%v2_comments}}', 'message', 'json NOT NULL DEFAULT \'[]\''); - $this->addColumn('{{%v2_comments}}', 'author_id', $this->integer(11)->notNull()); + $this->addColumn('{{%v2_comments}}', 'author_id', $this->integer(11)->notNull()->after('post_id')); $this->dropColumn('{{%v2_comments}}', 'user_id'); $this->addForeignKey('fk_v2_comments_author_id_v2_users_id', '{{%v2_comments}}', 'id', 'v2_users', 'author_id'); $this->addForeignKey('fk_v2_comments_post_id_v2_posts_uid', '{{%v2_comments}}', 'uid', 'v2_posts', 'post_id'); diff --git a/tests/specs/blog_v2/migrations_mysql_db/m200000_000004_change_table_v2_users.php b/tests/specs/blog_v2/migrations_mysql_db/m200000_000004_change_table_v2_users.php index b7a1f5e3..412d6ade 100644 --- a/tests/specs/blog_v2/migrations_mysql_db/m200000_000004_change_table_v2_users.php +++ b/tests/specs/blog_v2/migrations_mysql_db/m200000_000004_change_table_v2_users.php @@ -25,7 +25,7 @@ public function down() $this->alterColumn('{{%v2_users}}', 'created_at', $this->timestamp()->null()->defaultExpression("CURRENT_TIMESTAMP")); $this->alterColumn('{{%v2_users}}', 'role', $this->string(20)->null()->defaultValue('reader')); $this->alterColumn('{{%v2_users}}', 'email', $this->string(200)->notNull()); - $this->addColumn('{{%v2_users}}', 'username', $this->string(200)->notNull()); + $this->addColumn('{{%v2_users}}', 'username', $this->string(200)->notNull()->after('id')); $this->dropColumn('{{%v2_users}}', 'login'); } } diff --git a/tests/specs/blog_v2/migrations_mysql_db/m200000_000005_change_table_v2_comments.php b/tests/specs/blog_v2/migrations_mysql_db/m200000_000005_change_table_v2_comments.php index 5861542c..497550fc 100644 --- a/tests/specs/blog_v2/migrations_mysql_db/m200000_000005_change_table_v2_comments.php +++ b/tests/specs/blog_v2/migrations_mysql_db/m200000_000005_change_table_v2_comments.php @@ -25,7 +25,7 @@ public function down() $this->alterColumn('{{%v2_comments}}', 'created_at', $this->integer()->notNull()); $this->alterColumn('{{%v2_comments}}', 'meta_data', 'json NOT NULL'); $this->alterColumn('{{%v2_comments}}', 'message', 'json NOT NULL'); - $this->addColumn('{{%v2_comments}}', 'author_id', $this->integer()->notNull()); + $this->addColumn('{{%v2_comments}}', 'author_id', $this->integer()->notNull()->after('post_id')); $this->dropColumn('{{%v2_comments}}', 'user_id'); $this->addForeignKey('fk_v2_comments_author_id_v2_users_id', '{{%v2_comments}}', 'id', 'v2_users', 'author_id'); $this->addForeignKey('fk_v2_comments_post_id_v2_posts_uid', '{{%v2_comments}}', 'uid', 'v2_posts', 'post_id'); diff --git a/tests/specs/issue_fix/58_create_migration_for_column_position_change_if_a_field_position_is_changed_in_spec/index.php b/tests/specs/issue_fix/58_create_migration_for_column_position_change_if_a_field_position_is_changed_in_spec/index.php new file mode 100644 index 00000000..5c16da25 --- /dev/null +++ b/tests/specs/issue_fix/58_create_migration_for_column_position_change_if_a_field_position_is_changed_in_spec/index.php @@ -0,0 +1,13 @@ + '@specs/issue_fix/58_create_migration_for_column_position_change_if_a_field_position_is_changed_in_spec/index.yml', + 'generateUrls' => false, + 'generateModels' => false, + 'excludeModels' => [ + 'Error', + ], + 'generateControllers' => false, + 'generateMigrations' => true, + 'generateModelFaker' => false, // `generateModels` must be `true` in order to use `generateModelFaker` as `true` +]; diff --git a/tests/specs/issue_fix/58_create_migration_for_column_position_change_if_a_field_position_is_changed_in_spec/index.yml b/tests/specs/issue_fix/58_create_migration_for_column_position_change_if_a_field_position_is_changed_in_spec/index.yml new file mode 100644 index 00000000..3c9a1e63 --- /dev/null +++ b/tests/specs/issue_fix/58_create_migration_for_column_position_change_if_a_field_position_is_changed_in_spec/index.yml @@ -0,0 +1,24 @@ +openapi: 3.0.3 + +info: + title: 'Create migration for column position change if a field position is changed in spec #58' + version: 1.0.0 + +components: + schemas: + Fruit: + type: object + properties: + id: + type: integer + name: + type: string + description: + type: string + +paths: + '/': + get: + responses: + '200': + description: OK diff --git a/tests/specs/issue_fix/58_create_migration_for_column_position_change_if_a_field_position_is_changed_in_spec/mysql/migrations_mysql_db/m200000_000000_change_table_fruits.php b/tests/specs/issue_fix/58_create_migration_for_column_position_change_if_a_field_position_is_changed_in_spec/mysql/migrations_mysql_db/m200000_000000_change_table_fruits.php new file mode 100644 index 00000000..90ab0c79 --- /dev/null +++ b/tests/specs/issue_fix/58_create_migration_for_column_position_change_if_a_field_position_is_changed_in_spec/mysql/migrations_mysql_db/m200000_000000_change_table_fruits.php @@ -0,0 +1,17 @@ +alterColumn('{{%fruits}}', 'name', $this->text()->null()->after('id')); + } + + public function down() + { + $this->alterColumn('{{%fruits}}', 'name', $this->text()->null()->after('description')); + } +} diff --git a/tests/specs/new_column_position/maria/app/migrations_maria_db/m200000_000003_change_table_dropfirsttwocols.php b/tests/specs/new_column_position/maria/app/migrations_maria_db/m200000_000003_change_table_dropfirsttwocols.php index 89ef1937..b67baf29 100644 --- a/tests/specs/new_column_position/maria/app/migrations_maria_db/m200000_000003_change_table_dropfirsttwocols.php +++ b/tests/specs/new_column_position/maria/app/migrations_maria_db/m200000_000003_change_table_dropfirsttwocols.php @@ -7,13 +7,13 @@ class m200000_000003_change_table_dropfirsttwocols extends \yii\db\Migration { public function up() { - $this->dropColumn('{{%dropfirsttwocols}}', 'name'); $this->dropColumn('{{%dropfirsttwocols}}', 'address'); + $this->dropColumn('{{%dropfirsttwocols}}', 'name'); } public function down() { - $this->addColumn('{{%dropfirsttwocols}}', 'address', $this->text()->null()->defaultValue(null)); $this->addColumn('{{%dropfirsttwocols}}', 'name', $this->text()->null()->defaultValue(null)->first()); + $this->addColumn('{{%dropfirsttwocols}}', 'address', $this->text()->null()->defaultValue(null)->after('name')); } } diff --git a/tests/specs/new_column_position/mysql/app/migrations_mysql_db/m200000_000003_change_table_dropfirsttwocols.php b/tests/specs/new_column_position/mysql/app/migrations_mysql_db/m200000_000003_change_table_dropfirsttwocols.php index aaa54835..83aa17eb 100644 --- a/tests/specs/new_column_position/mysql/app/migrations_mysql_db/m200000_000003_change_table_dropfirsttwocols.php +++ b/tests/specs/new_column_position/mysql/app/migrations_mysql_db/m200000_000003_change_table_dropfirsttwocols.php @@ -7,13 +7,13 @@ class m200000_000003_change_table_dropfirsttwocols extends \yii\db\Migration { public function up() { - $this->dropColumn('{{%dropfirsttwocols}}', 'name'); $this->dropColumn('{{%dropfirsttwocols}}', 'address'); + $this->dropColumn('{{%dropfirsttwocols}}', 'name'); } public function down() { - $this->addColumn('{{%dropfirsttwocols}}', 'address', $this->text()->null()); $this->addColumn('{{%dropfirsttwocols}}', 'name', $this->text()->null()->first()); + $this->addColumn('{{%dropfirsttwocols}}', 'address', $this->text()->null()->after('name')); } } diff --git a/tests/unit/IssueFixTest.php b/tests/unit/IssueFixTest.php index b6c7abdb..1c745376 100644 --- a/tests/unit/IssueFixTest.php +++ b/tests/unit/IssueFixTest.php @@ -360,4 +360,1321 @@ public function test158BugGiiapiGeneratedRulesEnumWithTrim() ]); $this->checkFiles($actualFiles, $expectedFiles); } + + // https://github.com/php-openapi/yii2-openapi/issues/58 + public function test58CreateMigrationForColumnPositionChange() + { + $this->deleteTableFor58CreateMigrationForColumnPositionChange(); + $this->createTableFor58CreateMigrationForColumnPositionChange(); + + $testFile = Yii::getAlias("@specs/issue_fix/58_create_migration_for_column_position_change_if_a_field_position_is_changed_in_spec/index.php"); + $this->runGenerator($testFile); + $actualFiles = FileHelper::findFiles(Yii::getAlias('@app'), [ + 'recursive' => true, + ]); + $expectedFiles = FileHelper::findFiles(Yii::getAlias("@specs/issue_fix/58_create_migration_for_column_position_change_if_a_field_position_is_changed_in_spec/mysql"), [ + 'recursive' => true, + ]); + $this->checkFiles($actualFiles, $expectedFiles); + $this->runActualMigrations('mysql', 1); + $this->deleteTableFor58CreateMigrationForColumnPositionChange(); + } + + private function createTableFor58CreateMigrationForColumnPositionChange() + { + Yii::$app->db->createCommand()->createTable('{{%fruits}}', [ + 'id' => 'pk', + 'description' => 'text', + 'name' => 'text', + ])->execute(); + } + + private function deleteTableFor58CreateMigrationForColumnPositionChange() + { + Yii::$app->db->createCommand('DROP TABLE IF EXISTS {{%fruits}}')->execute(); + } + + private function for58($schema, $expected, $columns = [ + 'id' => 'pk', + 'name' => 'text not null', + 'description' => 'text not null', + 'colour' => 'text not null', + 'size' => 'text not null', + ], $dbs = ['Mysql', 'Mariadb']) + { + $deleteTable = function () { + Yii::$app->db->createCommand('DROP TABLE IF EXISTS {{%fruits}}')->execute(); + }; + $createTable = function () use ($columns) { + Yii::$app->db->createCommand()->createTable('{{%fruits}}', $columns)->execute(); + }; + + $config = [ + 'openApiPath' => 'data://text/plain;base64,' . base64_encode($schema), + 'generateUrls' => false, + 'generateModels' => false, + 'generateControllers' => false, + 'generateMigrations' => true, + 'generateModelFaker' => false, + ]; + $tmpConfigFile = Yii::getAlias("@runtime") . "/tmp-config.php"; + file_put_contents($tmpConfigFile, '{"changeDbTo$db"}(); + $deleteTable(); + $createTable(); + + $dbStr = str_replace('db', '', strtolower($db)); + $this->runGenerator($tmpConfigFile, $dbStr); + $actual = file_get_contents(Yii::getAlias('@app') . '/migrations_' . $dbStr . '_db/m200000_000000_change_table_fruits.php'); + $this->assertSame($expected, $actual); + $this->runActualMigrations($dbStr, 1); + $deleteTable(); + } + FileHelper::unlink($tmpConfigFile); + } + + // ------------ Delete + public function test58DeleteLastCol() + { + $schema = <<dropColumn('{{%fruits}}', 'size'); + } + + public function down() + { + $this->addColumn('{{%fruits}}', 'size', $this->text()->notNull()); + } +} + +PHP; + + $this->for58($schema, $expected); + } + + public function test58DeleteLast2ConsecutiveCol() + { + $schema = <<dropColumn('{{%fruits}}', 'size'); + $this->dropColumn('{{%fruits}}', 'colour'); + } + + public function down() + { + $this->addColumn('{{%fruits}}', 'colour', $this->text()->notNull()); + $this->addColumn('{{%fruits}}', 'size', $this->text()->notNull()); + } +} + +PHP; + + $this->for58($schema, $expected); + } + + public function test58DeleteAColInBetween() + { + $schema = <<dropColumn('{{%fruits}}', 'description'); + } + + public function down() + { + $this->addColumn('{{%fruits}}', 'description', $this->text()->notNull()->after('name')); + } +} + +PHP; + + $this->for58($schema, $expected); + } + + public function test58Delete2ConsecutiveColInBetween() + { + $schema = <<dropColumn('{{%fruits}}', 'colour'); + $this->dropColumn('{{%fruits}}', 'description'); + } + + public function down() + { + $this->addColumn('{{%fruits}}', 'description', $this->text()->notNull()->after('name')); + $this->addColumn('{{%fruits}}', 'colour', $this->text()->notNull()->after('description')); + } +} + +PHP; + + $this->for58($schema, $expected); + } + + public function test58Delete2NonConsecutiveColInBetween() + { + $schema = <<dropColumn('{{%fruits}}', 'colour'); + $this->dropColumn('{{%fruits}}', 'name'); + } + + public function down() + { + $this->addColumn('{{%fruits}}', 'name', $this->text()->notNull()->after('id')); + $this->addColumn('{{%fruits}}', 'colour', $this->text()->notNull()->after('description')); + } +} + +PHP; + + $this->for58($schema, $expected); + } + + public function test58DeleteLast4Col() + { + $columns = [ + 'id' => 'pk', + 'name' => 'bool null', + 'description' => 'bool null', + 'colour' => 'bool null', + 'size' => 'bool null', + 'col_6' => 'bool null', + ]; + + $schema = <<dropColumn('{{%fruits}}', 'col_6'); + $this->dropColumn('{{%fruits}}', 'size'); + $this->dropColumn('{{%fruits}}', 'colour'); + $this->dropColumn('{{%fruits}}', 'description'); + } + + public function down() + { + $this->addColumn('{{%fruits}}', 'description', $this->tinyInteger(1)->null()->defaultValue(null)); + $this->addColumn('{{%fruits}}', 'colour', $this->tinyInteger(1)->null()->defaultValue(null)->after('description')); + $this->addColumn('{{%fruits}}', 'size', $this->tinyInteger(1)->null()->defaultValue(null)->after('colour')); + $this->addColumn('{{%fruits}}', 'col_6', $this->tinyInteger(1)->null()->defaultValue(null)); + } +} + +PHP; + + $this->for58($schema, $expected, $columns); + } + + public function test58DeleteFirst4Col() + { + $columns = [ + 'name' => 'boolean null', + 'description' => 'boolean null', + 'colour' => 'boolean null', + 'size' => 'boolean null', + 'col_6' => 'boolean null', + 'col_7' => 'boolean null', + ]; + + $schema = <<dropColumn('{{%fruits}}', 'size'); + $this->dropColumn('{{%fruits}}', 'colour'); + $this->dropColumn('{{%fruits}}', 'description'); + $this->dropColumn('{{%fruits}}', 'name'); + } + + public function down() + { + $this->addColumn('{{%fruits}}', 'name', $this->tinyInteger(1)->null()->defaultValue(null)->first()); + $this->addColumn('{{%fruits}}', 'description', $this->tinyInteger(1)->null()->defaultValue(null)->after('name')); + $this->addColumn('{{%fruits}}', 'colour', $this->tinyInteger(1)->null()->defaultValue(null)->after('description')); + $this->addColumn('{{%fruits}}', 'size', $this->tinyInteger(1)->null()->defaultValue(null)->after('colour')); + } +} + +PHP; + + $this->for58($schema, $expected, $columns); + } + + // ------------ Add + public function test58AddAColAtLastPos() + { + // default position is last so no `AFTER` needed + $schema = <<addColumn('{{%fruits}}', 'weight', $this->text()->notNull()); + } + + public function down() + { + $this->dropColumn('{{%fruits}}', 'weight'); + } +} + +PHP; + + $this->for58($schema, $expected); + } + + public function test58Add2ConsecutiveColAtLastPos() + { + $schema = <<addColumn('{{%fruits}}', 'weight', $this->text()->notNull()->after('size')); + $this->addColumn('{{%fruits}}', 'location', $this->text()->notNull()); + } + + public function down() + { + $this->dropColumn('{{%fruits}}', 'location'); + $this->dropColumn('{{%fruits}}', 'weight'); + } +} + +PHP; + + $this->for58($schema, $expected); + } + + public function test58AddAColInBetween() + { + $schema = <<addColumn('{{%fruits}}', 'weight', $this->text()->notNull()->after('description')); + } + + public function down() + { + $this->dropColumn('{{%fruits}}', 'weight'); + } +} + +PHP; + + $this->for58($schema, $expected); + } + + public function test58Add2ConsecutiveColInBetween() + { + $schema = <<addColumn('{{%fruits}}', 'weight', $this->text()->notNull()->after('description')); + $this->addColumn('{{%fruits}}', 'location', $this->text()->notNull()->after('weight')); + } + + public function down() + { + $this->dropColumn('{{%fruits}}', 'location'); + $this->dropColumn('{{%fruits}}', 'weight'); + } +} + +PHP; + + $this->for58($schema, $expected); + } + + public function test58Add2NonConsecutiveColInBetween() + { + $schema = <<addColumn('{{%fruits}}', 'weight', $this->text()->notNull()->after('name')); + $this->addColumn('{{%fruits}}', 'location', $this->text()->notNull()->after('colour')); + } + + public function down() + { + $this->dropColumn('{{%fruits}}', 'location'); + $this->dropColumn('{{%fruits}}', 'weight'); + } +} + +PHP; + + $this->for58($schema, $expected); + } + + // ------------ Just move columns + public function test58MoveLast2Col2PosUp() + { + $columns = [ + 'id' => 'pk', + 'name' => 'bool null', + 'description' => 'bool null', + 'colour' => 'bool null', + 'size' => 'bool null', + ]; + + $schema = <<alterColumn('{{%fruits}}', 'colour', $this->tinyInteger(1)->null()->defaultValue(null)->after('id')); + $this->alterColumn('{{%fruits}}', 'size', $this->tinyInteger(1)->null()->defaultValue(null)->after('colour')); + } + + public function down() + { + $this->alterColumn('{{%fruits}}', 'size', $this->tinyInteger(1)->null()->defaultValue(null)->after('colour')); + $this->alterColumn('{{%fruits}}', 'colour', $this->tinyInteger(1)->null()->defaultValue(null)->after('description')); + } +} + +PHP; + + $this->for58($schema, $expected, $columns); + } + + // ----------- Miscellaneous + public function test58Move1Add1Del1Col() + { + $columns = [ + 'id' => 'pk', + 'name' => 'boolean null', + 'description' => 'boolean null', + 'colour' => 'boolean null', + 'size' => 'boolean null', + ]; + + $schema = <<addColumn('{{%fruits}}', 'col_6', $this->boolean()->null()->defaultValue(null)); + $this->dropColumn('{{%fruits}}', 'size'); + $this->alterColumn('{{%fruits}}', 'colour', $this->tinyInteger(1)->null()->defaultValue(null)->after('id')); + } + + public function down() + { + $this->alterColumn('{{%fruits}}', 'colour', $this->tinyInteger(1)->null()->defaultValue(null)->after('description')); + $this->addColumn('{{%fruits}}', 'size', $this->tinyInteger(1)->null()->defaultValue(null)); + $this->dropColumn('{{%fruits}}', 'col_6'); + } +} + +PHP; + + $this->for58($schema, $expected, $columns); + } + + public function test58Add1Del1ColAtSamePosition() + { + $columns = [ + 'id' => 'pk', + 'name' => 'bool null', + 'description' => 'bool null', + 'colour' => 'bool null', + 'size' => 'bool null', + ]; + + $schema = <<addColumn('{{%fruits}}', 'description_new', $this->boolean()->null()->defaultValue(null)->after('name')); + $this->dropColumn('{{%fruits}}', 'description'); + } + + public function down() + { + $this->addColumn('{{%fruits}}', 'description', $this->tinyInteger(1)->null()->defaultValue(null)->after('name')); + $this->dropColumn('{{%fruits}}', 'description_new'); + } +} + +PHP; + + $this->for58($schema, $expected, $columns); + } + + public function test58Add3Del2ColAtDiffPos() + { + $columns = [ + 'id' => 'pk', + 'name' => 'bool null', + 'description' => 'bool null', + 'colour' => 'bool null', + 'size' => 'bool null', + ]; + + $schema = <<addColumn('{{%fruits}}', 'col_6', $this->boolean()->null()->defaultValue(null)->after('id')); + $this->addColumn('{{%fruits}}', 'col_7', $this->boolean()->null()->defaultValue(null)->after('name')); + $this->addColumn('{{%fruits}}', 'col_8', $this->boolean()->null()->defaultValue(null)->after('col_7')); + $this->dropColumn('{{%fruits}}', 'colour'); + $this->dropColumn('{{%fruits}}', 'description'); + } + + public function down() + { + $this->addColumn('{{%fruits}}', 'description', $this->tinyInteger(1)->null()->defaultValue(null)->after('name')); + $this->addColumn('{{%fruits}}', 'colour', $this->tinyInteger(1)->null()->defaultValue(null)->after('description')); + $this->dropColumn('{{%fruits}}', 'col_8'); + $this->dropColumn('{{%fruits}}', 'col_7'); + $this->dropColumn('{{%fruits}}', 'col_6'); + } +} + +PHP; + + $this->for58($schema, $expected, $columns); + } + + // This test fails. See description of https://github.com/php-openapi/yii2-openapi/pull/59 +// public function test58Add3Del2Move3ColAtDiffPos() +// { +// $columns = [ +// 'id' => 'pk', +// 'name' => 'bool null', +// 'description' => 'bool null', +// 'colour' => 'bool null', +// 'size' => 'bool null', +// 'col_6' => 'bool null', +// ]; +// +// $schema = <<for58($schema, $expected, $columns); +// } + + public function test58MoveAColAndChangeItsDataType() + { + $columns = [ + 'id' => 'pk', + 'name' => 'bool null', + 'description' => 'bool null', + 'colour' => 'bool null', + 'size' => 'bool null', + ]; + + $schema = <<alterColumn('{{%fruits}}', 'colour', $this->integer()->null()->defaultValue(null)); + $this->alterColumn('{{%fruits}}', 'name', $this->tinyInteger(1)->null()->defaultValue(null)->after('colour')); + } + + public function down() + { + $this->alterColumn('{{%fruits}}', 'name', $this->tinyInteger(1)->null()->defaultValue(null)->after('id')); + $this->alterColumn('{{%fruits}}', 'colour', $this->tinyInteger(1)->null()->defaultValue(null)); + } +} + +PHP; + + $this->for58($schema, $expected, $columns); + } + + public function test58MoveAColDownwards() + { + $columns = [ + 'id' => 'pk', + 'name' => 'bool null', + 'description' => 'bool null', + 'colour' => 'bool null', + 'size' => 'bool null', + ]; + + $schema = <<alterColumn('{{%fruits}}', 'name', $this->tinyInteger(1)->null()->defaultValue(null)->after('colour')); + } + + public function down() + { + $this->alterColumn('{{%fruits}}', 'name', $this->tinyInteger(1)->null()->defaultValue(null)->after('id')); + } +} + +PHP; + + $this->for58($schema, $expected, $columns); + } + + public function test58MoveAColUpwards() + { + $columns = [ + 'id' => 'pk', + 'name' => 'bool null', + 'description' => 'bool null', + 'colour' => 'bool null', + 'size' => 'bool null', + ]; + + $schema = <<alterColumn('{{%fruits}}', 'colour', $this->tinyInteger(1)->null()->defaultValue(null)->after('id')); + } + + public function down() + { + $this->alterColumn('{{%fruits}}', 'colour', $this->tinyInteger(1)->null()->defaultValue(null)->after('description')); + } +} + +PHP; + + $this->for58($schema, $expected, $columns); + } }