Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

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