diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a8677d275b..d02aba13e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,10 @@ The present file will list all changes made to the project; according to the ### API changes +#### Added + +- `Migration::renameItemtype()` method to update of database schema/values when an itemtype class is renamed + #### Changes - `DBmysqlIterator::handleOrderClause()` supports QueryExpressions diff --git a/inc/migration.class.php b/inc/migration.class.php index 75a37bb9b7f..eec01247d97 100644 --- a/inc/migration.class.php +++ b/inc/migration.class.php @@ -1229,4 +1229,146 @@ private function outputMessageToHtml($msg, $style = null, $area_id = null) { echo $msg; } } + + /** + * Rename an itemtype an update database structure and data to use the new itemtype name. + * Changes done by this method: + * - renaming of itemtype table; + * - renaming of foreign key fields corresponding to this itemtype; + * - update of "itemtype" column values in all tables. + * + * @param string $old_itemtype + * @param string $new_itemtype + * + * @return void + * + * @since 9.5.0 + */ + public function renameItemtype($old_itemtype, $new_itemtype) { + global $DB; + + if ($old_itemtype == $new_itemtype) { + // Do nothing if new value is same as old one + return; + } + + $this->displayTitle(sprintf('Rename "%s" itemtype to "%s"', $old_itemtype, $new_itemtype)); + + $old_table = getTableForItemType($old_itemtype); + $new_table = getTableForItemType($new_itemtype); + $old_fkey = getForeignKeyFieldForItemType($old_itemtype); + $new_fkey = getForeignKeyFieldForItemType($new_itemtype); + + // Check prerequisites + if (!$DB->tableExists($old_table)) { + throw new \RuntimeException( + sprintf( + 'Table "%s" does not exists.', + $old_table, + $new_table + ) + ); + } + if ($DB->tableExists($new_table)) { + throw new \RuntimeException( + sprintf( + 'Table "%s" cannot be renamed as table "%s" already exists.', + $old_table, + $new_table + ) + ); + } + $fkey_column_iterator = $DB->request( + [ + 'SELECT' => [ + 'table_name AS TABLE_NAME', + 'column_name AS COLUMN_NAME', + ], + 'FROM' => 'information_schema.columns', + 'WHERE' => [ + 'table_schema' => $DB->dbdefault, + 'table_name' => ['LIKE', 'glpi_%'], + 'OR' => [ + ['column_name' => $old_fkey], + ['column_name' => ['LIKE', $old_fkey . '_%']], + ], + ], + 'ORDER' => 'TABLE_NAME', + ] + ); + $fkey_column_array = iterator_to_array($fkey_column_iterator); // Convert to array to be able to loop twice + foreach ($fkey_column_array as $fkey_column) { + $fkey_table = $fkey_column['TABLE_NAME']; + $fkey_oldname = $fkey_column['COLUMN_NAME']; + $fkey_newname = preg_replace('/^' . preg_quote($old_fkey) . '/', $new_fkey, $fkey_oldname); + if ($DB->fieldExists($fkey_table, $fkey_newname)) { + throw new \RuntimeException( + sprintf( + 'Field "%s" cannot be renamed in table "%s" as "%s" is field already exists.', + $fkey_oldname, + $fkey_table, + $fkey_newname + ) + ); + } + } + + //1. Rename itemtype table + $this->displayMessage(sprintf('Rename "%s" table to "%s"', $old_table, $new_table)); + $this->renameTable($old_table, $new_table); + + //2. Rename foreign key fields + $this->displayMessage( + sprintf('Rename "%s" foreign keys to "%s" in all tables', $old_fkey, $new_fkey) + ); + foreach ($fkey_column_array as $fkey_column) { + $fkey_table = $fkey_column['TABLE_NAME']; + $fkey_oldname = $fkey_column['COLUMN_NAME']; + $fkey_newname = preg_replace('/^' . preg_quote($old_fkey) . '/', $new_fkey, $fkey_oldname); + + if ($fkey_table == $old_table) { + // Special case, foreign key is inside renamed table, use new name + $fkey_table = $new_table; + } + + $this->changeField( + $fkey_table, + $fkey_oldname, + $fkey_newname, + 'integer' // assume that foreign key always uses integer type + ); + } + + //3. Update "itemtype" values in all tables + $this->displayMessage( + sprintf('Rename "%s" itemtype to "%s" in all tables', $old_itemtype, $new_itemtype) + ); + $itemtype_column_iterator = $DB->request( + [ + 'SELECT' => [ + 'table_name AS TABLE_NAME', + 'column_name AS COLUMN_NAME', + ], + 'FROM' => 'information_schema.columns', + 'WHERE' => [ + 'table_schema' => $DB->dbdefault, + 'table_name' => ['LIKE', 'glpi_%'], + 'OR' => [ + ['column_name' => 'itemtype'], + ['column_name' => ['LIKE', 'itemtype_%']], + ], + ], + 'ORDER' => 'TABLE_NAME', + ] + ); + foreach ($itemtype_column_iterator as $itemtype_column) { + $this->addPostQuery( + $DB->buildUpdate( + $itemtype_column['TABLE_NAME'], + [$itemtype_column['COLUMN_NAME'] => $new_itemtype], + [$itemtype_column['COLUMN_NAME'] => $old_itemtype] + ) + ); + } + } } diff --git a/tests/units/Migration.php b/tests/units/Migration.php index c1fd7f9dc2b..c653a1b7a2a 100644 --- a/tests/units/Migration.php +++ b/tests/units/Migration.php @@ -609,4 +609,139 @@ public function testRenameTable() { ] ); } + + /** + * Test Migration::renameItemtype(). + * Case: failure as source table does not exists. + */ + public function testRenameItemtypeWhenSourceTableDoesNotExists() { + global $DB; + $DB = $this->db; + + $this->calling($this->db)->tableExists = false; + + $migration = $this->migration; + $this->exception( + function() use ($migration) { + $migration->renameItemtype('SomeOldType', 'NewName'); + } + )->isInstanceOf(\RuntimeException::class) + ->message + ->contains('Table "glpi_someoldtypes" does not exists.'); + } + + /** + * Test Migration::renameItemtype(). + * Case: failure as destination table already exists. + */ + public function testRenameItemtypeWhenDestinationTableAlreadyExists() { + global $DB; + $DB = $this->db; + + $this->calling($this->db)->tableExists = true; + + $this->exception( + function() { + $this->migration->renameItemtype('SomeOldType', 'NewName'); + } + )->isInstanceOf(\RuntimeException::class) + ->message + ->contains('Table "glpi_someoldtypes" cannot be renamed as table "glpi_newnames" already exists.'); + } + + /** + * Test Migration::renameItemtype(). + * Case: failure as foreign key field already in use somewhere. + */ + public function testRenameItemtypeWhenDestinationFieldAlreadyExists() { + global $DB; + $DB = $this->db; + + $this->calling($this->db)->tableExists = function ($table) { + return $table === 'glpi_someoldtypes'; + }; + $this->calling($this->db)->fieldExists = true; + $this->calling($this->db)->request = new \ArrayIterator([ + [ + 'TABLE_NAME' => 'glpi_item_with_fkey', 'COLUMN_NAME' => 'someoldtypes_id' + ] + ]); + + $this->exception( + function() { + $this->migration->renameItemtype('SomeOldType', 'NewName'); + } + )->isInstanceOf(\RuntimeException::class) + ->message + ->contains('Field "someoldtypes_id" cannot be renamed in table "glpi_item_with_fkey" as "newnames_id" is field already exists.'); + } + + /** + * Test Migration::renameItemtype(). + * Case: success. + */ + public function testRenameItemtype() { + global $DB; + $DB = $this->db; + + $this->calling($this->db)->tableExists = function ($table) { + return $table === 'glpi_someoldtypes'; + }; + $this->calling($this->db)->fieldExists = function ($table, $field) { + return preg_match('/^someoldtypes_id/', $field); + }; + $this->calling($this->db)->request = function ($request) { + if (isset($request['WHERE']['OR'][0]) + && $request['WHERE']['OR'][0] === ['column_name' => 'someoldtypes_id']) { + // Request used for foreign key fields + return new \ArrayIterator([ + ['TABLE_NAME' => 'glpi_oneitem_with_fkey', 'COLUMN_NAME' => 'someoldtypes_id'], + ['TABLE_NAME' => 'glpi_anotheritem_with_fkey', 'COLUMN_NAME' => 'someoldtypes_id'], + ['TABLE_NAME' => 'glpi_anotheritem_with_fkey', 'COLUMN_NAME' => 'someoldtypes_id_tech'], + ]); + } + if (isset($request['WHERE']['OR'][0]) + && $request['WHERE']['OR'][0] === ['column_name' => 'itemtype']) { + // Request used for itemtype fields + return new \ArrayIterator([ + ['TABLE_NAME' => 'glpi_computers', 'COLUMN_NAME' => 'itemtype'], + ['TABLE_NAME' => 'glpi_users', 'COLUMN_NAME' => 'itemtype'], + ['TABLE_NAME' => 'glpi_stuffs', 'COLUMN_NAME' => 'itemtype_source'], + ['TABLE_NAME' => 'glpi_stuffs', 'COLUMN_NAME' => 'itemtype_dest'], + ]); + } + return []; + }; + + $this->output( + function () { + $this->migration->renameItemtype('SomeOldType', 'NewName'); + $this->migration->executeMigration(); + } + )->isIdenticalTo( + implode( + '', + [ + '============================ Rename "SomeOldType" itemtype to "NewName" ============================' . "\n", + 'Rename "glpi_someoldtypes" table to "glpi_newnames"', + 'Rename "someoldtypes_id" foreign keys to "newnames_id" in all tables', + 'Rename "SomeOldType" itemtype to "NewName" in all tables', + 'Change of the database layout - glpi_oneitem_with_fkey', + 'Change of the database layout - glpi_anotheritem_with_fkey', + 'Task completed.', + ] + ) + ); + + $this->array($this->queries)->isIdenticalTo([ + "RENAME TABLE `glpi_someoldtypes` TO `glpi_newnames`", + "ALTER TABLE `glpi_oneitem_with_fkey` CHANGE `someoldtypes_id` `newnames_id` INT(11) NOT NULL DEFAULT '0' ", + "ALTER TABLE `glpi_anotheritem_with_fkey` CHANGE `someoldtypes_id` `newnames_id` INT(11) NOT NULL DEFAULT '0' ,\n" + . "CHANGE `someoldtypes_id_tech` `newnames_id_tech` INT(11) NOT NULL DEFAULT '0' ", + "UPDATE `glpi_computers` SET `itemtype` = 'NewName' WHERE `itemtype` = 'SomeOldType'", + "UPDATE `glpi_users` SET `itemtype` = 'NewName' WHERE `itemtype` = 'SomeOldType'", + "UPDATE `glpi_stuffs` SET `itemtype_source` = 'NewName' WHERE `itemtype_source` = 'SomeOldType'", + "UPDATE `glpi_stuffs` SET `itemtype_dest` = 'NewName' WHERE `itemtype_dest` = 'SomeOldType'", + ]); + } }