Skip to content

Commit

Permalink
Add itemtype migration method
Browse files Browse the repository at this point in the history
  • Loading branch information
cedric-anne authored and trasher committed Sep 27, 2019
1 parent a8f2d3f commit d85a10d
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
142 changes: 142 additions & 0 deletions inc/migration.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]
)
);
}
}
}
135 changes: 135 additions & 0 deletions tests/units/Migration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'",
]);
}
}

0 comments on commit d85a10d

Please sign in to comment.