diff --git a/src/Phinx/Db/Adapter/SQLiteAdapter.php b/src/Phinx/Db/Adapter/SQLiteAdapter.php index 0ccde21c58..23dbf15eb3 100644 --- a/src/Phinx/Db/Adapter/SQLiteAdapter.php +++ b/src/Phinx/Db/Adapter/SQLiteAdapter.php @@ -734,7 +734,11 @@ protected function getAddColumnInstructions(Table $table, Column $column): Alter return $newState + $state; }); - return $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->bufferIndicesAndTriggers($instructions, $tableName); + $instructions = $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->recreateIndicesAndTriggers($instructions); + + return $instructions; } /** @@ -793,6 +797,178 @@ 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; + } + /** * Copies all the data from a tmp table to another table * @@ -832,20 +1008,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 +1015,6 @@ protected function copyAndDropTmpTable(AlterInstructions $instructions, string $ $this->quoteTableName($tableName) )); - foreach ($rows as $row) { - $this->execute($row['sql']); - } - return $state; }); @@ -969,7 +1127,12 @@ protected function getRenameColumnInstructions(string $tableName, string $column return $newState + $state; }); - return $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->bufferIndicesAndTriggers($instructions, $tableName); + $instructions = $this->updateIndicesForRenamedColumn($instructions, $columnName, $newColumnName); + $instructions = $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->recreateIndicesAndTriggers($instructions); + + return $instructions; } /** @@ -998,7 +1161,11 @@ protected function getChangeColumnInstructions(string $tableName, string $column return $newState + $state; }); - return $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->bufferIndicesAndTriggers($instructions, $tableName); + $instructions = $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->recreateIndicesAndTriggers($instructions); + + return $instructions; } /** @@ -1030,7 +1197,12 @@ protected function getDropColumnInstructions(string $tableName, string $columnNa return $state; }); - return $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->bufferIndicesAndTriggers($instructions, $tableName); + $instructions = $this->filterIndicesForRemovedColumn($instructions, $columnName); + $instructions = $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->recreateIndicesAndTriggers($instructions); + + return $instructions; } /** @@ -1306,7 +1478,11 @@ protected function getAddPrimaryKeyInstructions(Table $table, string $column): A return compact('selectColumns', 'writeColumns') + $state; }); - return $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->bufferIndicesAndTriggers($instructions, $tableName); + $instructions = $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->recreateIndicesAndTriggers($instructions); + + return $instructions; } /** @@ -1316,7 +1492,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 +1512,12 @@ protected function getDropPrimaryKeyInstructions(Table $table, string $column): return $newState + $state; }); - return $this->copyAndDropTmpTable($instructions, $table->getName()); + $instructions = $this->bufferIndicesAndTriggers($instructions, $tableName); + $instructions = $this->filterIndicesForRemovedColumn($instructions, $column); + $instructions = $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->recreateIndicesAndTriggers($instructions); + + return $instructions; } /** @@ -1383,7 +1565,11 @@ protected function getAddForeignKeyInstructions(Table $table, ForeignKey $foreig return compact('selectColumns', 'writeColumns') + $state; }); - return $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->bufferIndicesAndTriggers($instructions, $tableName); + $instructions = $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->recreateIndicesAndTriggers($instructions); + + return $instructions; } /** @@ -1441,7 +1627,11 @@ protected function getDropForeignKeyByColumnsInstructions(string $tableName, arr return $newState + $state; }); - return $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->bufferIndicesAndTriggers($instructions, $tableName); + $instructions = $this->copyAndDropTmpTable($instructions, $tableName); + $instructions = $this->recreateIndicesAndTriggers($instructions); + + return $instructions; } /** diff --git a/tests/Phinx/Db/Adapter/SQLiteAdapterTest.php b/tests/Phinx/Db/Adapter/SQLiteAdapterTest.php index 936976af52..2b14555956 100644 --- a/tests/Phinx/Db/Adapter/SQLiteAdapterTest.php +++ b/tests/Phinx/Db/Adapter/SQLiteAdapterTest.php @@ -547,6 +547,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); @@ -709,6 +1035,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 [