PageRenderTime 47ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/admin/cli/mysql_collation.php

https://bitbucket.org/moodle/moodle
PHP | 328 lines | 254 code | 39 blank | 35 comment | 49 complexity | af14e5dd174ed2c3c7701405f10bad3a MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * MySQL collation conversion tool.
  18. *
  19. * @package core
  20. * @copyright 2012 Petr Skoda (http://skodak.org)
  21. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22. */
  23. define('CLI_SCRIPT', true);
  24. require(__DIR__.'/../../config.php');
  25. require_once($CFG->libdir.'/clilib.php'); // cli only functions
  26. if ($DB->get_dbfamily() !== 'mysql') {
  27. cli_error('This function is designed for MySQL databases only!');
  28. }
  29. // now get cli options
  30. list($options, $unrecognized) = cli_get_params(array('help'=>false, 'list'=>false, 'collation'=>false, 'available'=>false),
  31. array('h'=>'help', 'l'=>'list', 'a'=>'available'));
  32. if ($unrecognized) {
  33. $unrecognized = implode("\n ", $unrecognized);
  34. cli_error(get_string('cliunknowoption', 'admin', $unrecognized));
  35. }
  36. $help =
  37. "MySQL collation conversions script.
  38. It is strongly recommended to stop the web server before the conversion.
  39. This script may be executed before the main upgrade - 1.9.x data for example.
  40. Options:
  41. --collation=COLLATION Convert MySQL tables to different collation
  42. -l, --list Show table and column information
  43. -a, --available Show list of available collations
  44. -h, --help Print out this help
  45. Example:
  46. \$ sudo -u www-data /usr/bin/php admin/cli/mysql_collation.php --collation=utf8mb4_unicode_ci
  47. ";
  48. if (!empty($options['collation'])) {
  49. $collations = mysql_get_collations();
  50. $collation = clean_param($options['collation'], PARAM_ALPHANUMEXT);
  51. $collation = strtolower($collation);
  52. if (!isset($collations[$collation])) {
  53. cli_error("Error: collation '$collation' is not available on this server!");
  54. }
  55. $collationinfo = explode('_', $collation);
  56. $charset = reset($collationinfo);
  57. $engine = strtolower($DB->get_dbengine());
  58. // Do checks for utf8mb4.
  59. if (strpos($collation, 'utf8mb4') === 0) {
  60. // Do we have the right engine?
  61. if ($engine !== 'innodb' && $engine !== 'xtradb') {
  62. cli_error("Error: '$collation' requires InnoDB or XtraDB set as the engine.");
  63. }
  64. // Are we using Barracuda?
  65. if ($DB->get_row_format() != 'Barracuda') {
  66. // Try setting it here.
  67. try {
  68. $DB->execute("SET GLOBAL innodb_file_format=Barracuda");
  69. } catch (dml_exception $e) {
  70. cli_error("Error: '$collation' requires the file format to be set to Barracuda.
  71. An attempt was made to change the format, but it failed. Please try doing this manually.");
  72. }
  73. echo "GLOBAL SETTING: innodb_file_format changed to Barracuda\n";
  74. }
  75. // Is one file per table being used?
  76. if (!$DB->is_file_per_table_enabled()) {
  77. try {
  78. $DB->execute("SET GLOBAL innodb_file_per_table=1");
  79. } catch (dml_exception $e) {
  80. cli_error("Error: '$collation' requires the setting 'innodb_file_per_table' be set to 'ON'.
  81. An attempt was made to change the format, but it failed. Please try doing this manually.");
  82. }
  83. echo "GLOBAL SETTING: innodb_file_per_table changed to 1\n";
  84. }
  85. // Is large prefix set?
  86. if (!$DB->is_large_prefix_enabled()) {
  87. try {
  88. $DB->execute("SET GLOBAL innodb_large_prefix=1");
  89. } catch (dml_exception $e) {
  90. cli_error("Error: '$collation' requires the setting 'innodb_large_prefix' be set to 'ON'.
  91. An attempt was made to change the format, but it failed. Please try doing this manually.");
  92. }
  93. echo "GLOBAL SETTING: innodb_large_prefix changed to 1\n";
  94. }
  95. }
  96. $sql = "SHOW VARIABLES LIKE 'collation_database'";
  97. if (!$dbcollation = $DB->get_record_sql($sql)) {
  98. cli_error("Error: Could not access collation information on the database.");
  99. }
  100. $sql = "SHOW VARIABLES LIKE 'character_set_database'";
  101. if (!$dbcharset = $DB->get_record_sql($sql)) {
  102. cli_error("Error: Could not access character set information on the database.");
  103. }
  104. if ($dbcollation->value !== $collation || $dbcharset->value !== $charset) {
  105. // Try to convert the DB.
  106. echo "Converting database to '$collation' for $CFG->wwwroot:\n";
  107. $sql = "ALTER DATABASE `$CFG->dbname` DEFAULT CHARACTER SET $charset DEFAULT COLLATE = $collation";
  108. try {
  109. $DB->change_database_structure($sql);
  110. } catch (exception $e) {
  111. cli_error("Error: Tried to alter the database with no success. Please try manually changing the database
  112. to the new collation and character set and then run this script again.");
  113. }
  114. echo "DATABASE CONVERTED\n";
  115. }
  116. echo "Converting tables and columns to '$collation' for $CFG->wwwroot:\n";
  117. $prefix = $DB->get_prefix();
  118. $prefix = str_replace('_', '\\_', $prefix);
  119. $sql = "SHOW TABLE STATUS WHERE Name LIKE BINARY '$prefix%'";
  120. $rs = $DB->get_recordset_sql($sql);
  121. $converted = 0;
  122. $skipped = 0;
  123. $errors = 0;
  124. foreach ($rs as $table) {
  125. echo str_pad($table->name, 40). " - ";
  126. if ($table->collation === $collation) {
  127. echo "NO CHANGE\n";
  128. $skipped++;
  129. } else {
  130. try {
  131. $DB->change_database_structure("ALTER TABLE `$table->name` CONVERT TO CHARACTER SET $charset COLLATE $collation");
  132. echo "CONVERTED\n";
  133. $converted++;
  134. } catch (ddl_exception $e) {
  135. $result = mysql_set_row_format($table->name, $charset, $collation, $engine);
  136. if ($result) {
  137. echo "CONVERTED\n";
  138. $converted++;
  139. } else {
  140. // We don't know what the problem is. Stop the conversion.
  141. cli_error("Error: Tried to convert $table->name, but there was a problem. Please check the details of this
  142. table and try again.");
  143. die();
  144. }
  145. }
  146. }
  147. $sql = "SHOW FULL COLUMNS FROM `$table->name` WHERE collation IS NOT NULL";
  148. $rs2 = $DB->get_recordset_sql($sql);
  149. foreach ($rs2 as $column) {
  150. $column = (object)array_change_key_case((array)$column, CASE_LOWER);
  151. echo ' '.str_pad($column->field, 36). " - ";
  152. if ($column->collation === $collation) {
  153. echo "NO CHANGE\n";
  154. $skipped++;
  155. continue;
  156. }
  157. // Check for utf8mb4 collation.
  158. $rowformat = $DB->get_row_format_sql($engine, $collation);
  159. if ($column->type === 'tinytext' or $column->type === 'mediumtext' or $column->type === 'text' or $column->type === 'longtext') {
  160. $notnull = ($column->null === 'NO') ? 'NOT NULL' : 'NULL';
  161. $default = (!is_null($column->default) and $column->default !== '') ? "DEFAULT '$column->default'" : '';
  162. // primary, unique and inc are not supported for texts
  163. $sql = "ALTER TABLE `$table->name`
  164. MODIFY COLUMN $column->field $column->type
  165. CHARACTER SET $charset
  166. COLLATE $collation $notnull $default";
  167. $DB->change_database_structure($sql);
  168. } else if (strpos($column->type, 'varchar') === 0) {
  169. $notnull = ($column->null === 'NO') ? 'NOT NULL' : 'NULL';
  170. $default = !is_null($column->default) ? "DEFAULT '$column->default'" : '';
  171. if ($rowformat != '') {
  172. $sql = "ALTER TABLE `$table->name` $rowformat";
  173. $DB->change_database_structure($sql);
  174. }
  175. $sql = "ALTER TABLE `$table->name`
  176. MODIFY COLUMN $column->field $column->type
  177. CHARACTER SET $charset
  178. COLLATE $collation $notnull $default";
  179. $DB->change_database_structure($sql);
  180. } else {
  181. echo "ERROR (unknown column type: $column->type)\n";
  182. $errors++;
  183. continue;
  184. }
  185. echo "CONVERTED\n";
  186. $converted++;
  187. }
  188. $rs2->close();
  189. }
  190. $rs->close();
  191. echo "Converted: $converted, skipped: $skipped, errors: $errors\n";
  192. exit(0); // success
  193. } else if (!empty($options['list'])) {
  194. echo "List of tables for $CFG->wwwroot:\n";
  195. $prefix = $DB->get_prefix();
  196. $prefix = str_replace('_', '\\_', $prefix);
  197. $sql = "SHOW TABLE STATUS WHERE Name LIKE BINARY '$prefix%'";
  198. $rs = $DB->get_recordset_sql($sql);
  199. $counts = array();
  200. foreach ($rs as $table) {
  201. if (isset($counts[$table->collation])) {
  202. $counts[$table->collation]++;
  203. } else {
  204. $counts[$table->collation] = 1;
  205. }
  206. echo str_pad($table->name, 40);
  207. echo $table->collation. "\n";
  208. $collations = mysql_get_column_collations($table->name);
  209. foreach ($collations as $columname=>$collation) {
  210. if (isset($counts[$collation])) {
  211. $counts[$collation]++;
  212. } else {
  213. $counts[$collation] = 1;
  214. }
  215. echo ' ';
  216. echo str_pad($columname, 36);
  217. echo $collation. "\n";
  218. }
  219. }
  220. $rs->close();
  221. echo "\n";
  222. echo "Table collations summary for $CFG->wwwroot:\n";
  223. foreach ($counts as $collation => $count) {
  224. echo "$collation: $count\n";
  225. }
  226. exit(0); // success
  227. } else if (!empty($options['available'])) {
  228. echo "List of available MySQL collations for $CFG->wwwroot:\n";
  229. $collations = mysql_get_collations();
  230. foreach ($collations as $collation) {
  231. echo " $collation\n";
  232. }
  233. die;
  234. } else {
  235. echo $help;
  236. die;
  237. }
  238. // ========== Some functions ==============
  239. function mysql_get_collations() {
  240. global $DB;
  241. $collations = array();
  242. $sql = "SHOW COLLATION
  243. WHERE Collation LIKE 'utf8\_%' AND Charset = 'utf8'
  244. OR Collation LIKE 'utf8mb4\_%' AND Charset = 'utf8mb4'";
  245. $rs = $DB->get_recordset_sql($sql);
  246. foreach ($rs as $collation) {
  247. $collations[$collation->collation] = $collation->collation;
  248. }
  249. $rs->close();
  250. $collation = $DB->get_dbcollation();
  251. if (isset($collations[$collation])) {
  252. $collations[$collation] .= ' (default)';
  253. }
  254. return $collations;
  255. }
  256. function mysql_get_column_collations($tablename) {
  257. global $DB;
  258. $collations = array();
  259. $sql = "SELECT column_name, collation_name
  260. FROM INFORMATION_SCHEMA.COLUMNS
  261. WHERE table_schema = DATABASE() AND table_name = ? AND collation_name IS NOT NULL";
  262. $rs = $DB->get_recordset_sql($sql, array($tablename));
  263. foreach($rs as $record) {
  264. $collations[$record->column_name] = $record->collation_name;
  265. }
  266. $rs->close();
  267. return $collations;
  268. }
  269. function mysql_set_row_format($tablename, $charset, $collation, $engine) {
  270. global $DB;
  271. $sql = "SELECT row_format
  272. FROM INFORMATION_SCHEMA.TABLES
  273. WHERE table_schema = DATABASE() AND table_name = ?";
  274. $rs = $DB->get_record_sql($sql, array($tablename));
  275. if ($rs) {
  276. if ($rs->row_format == 'Compact' || $rs->row_format == 'Redundant') {
  277. $rowformat = $DB->get_row_format_sql($engine, $collation);
  278. // Try to convert to compressed format and then try updating the collation again.
  279. $DB->change_database_structure("ALTER TABLE `$tablename` $rowformat");
  280. $DB->change_database_structure("ALTER TABLE `$tablename` CONVERT TO CHARACTER SET $charset COLLATE $collation");
  281. } else {
  282. // Row format may not be the problem. Can not diagnose problem. Send fail reply.
  283. return false;
  284. }
  285. } else {
  286. return false;
  287. }
  288. return true;
  289. }