Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]

   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   * Database manager instance is responsible for all database structure modifications.
  19   *
  20   * @package    core_ddl
  21   * @copyright  1999 onwards Martin Dougiamas     http://dougiamas.com
  22   *             2001-3001 Eloy Lafuente (stronk7) http://contiento.com
  23   *             2008 Petr Skoda                   http://skodak.org
  24   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  25   */
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Database manager instance is responsible for all database structure modifications.
  31   *
  32   * It is using db specific generators to find out the correct SQL syntax to do that.
  33   *
  34   * @package    core_ddl
  35   * @copyright  1999 onwards Martin Dougiamas     http://dougiamas.com
  36   *             2001-3001 Eloy Lafuente (stronk7) http://contiento.com
  37   *             2008 Petr Skoda                   http://skodak.org
  38   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class database_manager {
  41  
  42      /** @var moodle_database A moodle_database driver specific instance.*/
  43      protected $mdb;
  44  
  45      /** @var sql_generator A driver specific SQL generator instance. Public because XMLDB editor needs to access it.*/
  46      public $generator;
  47  
  48      /**
  49       * Creates a new database manager instance.
  50       * @param moodle_database $mdb A moodle_database driver specific instance.
  51       * @param sql_generator $generator A driver specific SQL generator instance.
  52       */
  53      public function __construct($mdb, $generator) {
  54          $this->mdb       = $mdb;
  55          $this->generator = $generator;
  56      }
  57  
  58      /**
  59       * Releases all resources
  60       */
  61      public function dispose() {
  62          if ($this->generator) {
  63              $this->generator->dispose();
  64              $this->generator = null;
  65          }
  66          $this->mdb = null;
  67      }
  68  
  69      /**
  70       * This function will execute an array of SQL commands.
  71       *
  72       * @param string[] $sqlarr Array of sql statements to execute.
  73       * @param array|null $tablenames an array of xmldb table names affected by this request.
  74       * @throws ddl_change_structure_exception This exception is thrown if any error is found.
  75       */
  76      protected function execute_sql_arr(array $sqlarr, $tablenames = null) {
  77          $this->mdb->change_database_structure($sqlarr, $tablenames);
  78      }
  79  
  80      /**
  81       * Execute a given sql command string.
  82       *
  83       * @param string $sql The sql string you wish to be executed.
  84       * @throws ddl_change_structure_exception This exception is thrown if any error is found.
  85       */
  86      protected function execute_sql($sql) {
  87          $this->mdb->change_database_structure($sql);
  88      }
  89  
  90      /**
  91       * Given one xmldb_table, check if it exists in DB (true/false).
  92       *
  93       * @param string|xmldb_table $table The table to be searched (string name or xmldb_table instance).
  94       * @return bool True is a table exists, false otherwise.
  95       */
  96      public function table_exists($table) {
  97          if (!is_string($table) and !($table instanceof xmldb_table)) {
  98              throw new ddl_exception('ddlunknownerror', NULL, 'incorrect table parameter!');
  99          }
 100          return $this->generator->table_exists($table);
 101      }
 102  
 103      /**
 104       * Reset a sequence to the id field of a table.
 105       * @param string|xmldb_table $table Name of table.
 106       * @throws ddl_exception thrown upon reset errors.
 107       */
 108      public function reset_sequence($table) {
 109          if (!is_string($table) and !($table instanceof xmldb_table)) {
 110              throw new ddl_exception('ddlunknownerror', NULL, 'incorrect table parameter!');
 111          } else {
 112              if ($table instanceof xmldb_table) {
 113                  $tablename = $table->getName();
 114              } else {
 115                  $tablename = $table;
 116              }
 117          }
 118  
 119          // Do not test if table exists because it is slow
 120  
 121          if (!$sqlarr = $this->generator->getResetSequenceSQL($table)) {
 122              throw new ddl_exception('ddlunknownerror', null, 'table reset sequence sql not generated');
 123          }
 124  
 125          $this->execute_sql_arr($sqlarr, array($tablename));
 126      }
 127  
 128      /**
 129       * Given one xmldb_field, check if it exists in DB (true/false).
 130       *
 131       * @param string|xmldb_table $table The table to be searched (string name or xmldb_table instance).
 132       * @param string|xmldb_field $field The field to be searched for (string name or xmldb_field instance).
 133       * @return boolean true is exists false otherwise.
 134       * @throws ddl_table_missing_exception
 135       */
 136      public function field_exists($table, $field) {
 137          // Calculate the name of the table
 138          if (is_string($table)) {
 139              $tablename = $table;
 140          } else {
 141              $tablename = $table->getName();
 142          }
 143  
 144          // Check the table exists
 145          if (!$this->table_exists($table)) {
 146              throw new ddl_table_missing_exception($tablename);
 147          }
 148  
 149          if (is_string($field)) {
 150              $fieldname = $field;
 151          } else {
 152              // Calculate the name of the table
 153              $fieldname = $field->getName();
 154          }
 155  
 156          // Get list of fields in table
 157          $columns = $this->mdb->get_columns($tablename);
 158  
 159          $exists = array_key_exists($fieldname,  $columns);
 160  
 161          return $exists;
 162      }
 163  
 164      /**
 165       * Given one xmldb_index, the function returns the name of the index in DB
 166       * of false if it doesn't exist
 167       *
 168       * @param xmldb_table $xmldb_table table to be searched
 169       * @param xmldb_index $xmldb_index the index to be searched
 170       * @param bool $returnall true means return array of all indexes, false means first index only as string
 171       * @return array|string|bool Index name, array of index names or false if no indexes are found.
 172       * @throws ddl_table_missing_exception Thrown when table is not found.
 173       */
 174      public function find_index_name(xmldb_table $xmldb_table, xmldb_index $xmldb_index, $returnall = false) {
 175          // Calculate the name of the table
 176          $tablename = $xmldb_table->getName();
 177  
 178          // Check the table exists
 179          if (!$this->table_exists($xmldb_table)) {
 180              throw new ddl_table_missing_exception($tablename);
 181          }
 182  
 183          // Extract index columns
 184          $indcolumns = $xmldb_index->getFields();
 185  
 186          // Get list of indexes in table
 187          $indexes = $this->mdb->get_indexes($tablename);
 188  
 189          $return = array();
 190  
 191          // Iterate over them looking for columns coincidence
 192          foreach ($indexes as $indexname => $index) {
 193              $columns = $index['columns'];
 194              // Check if index matches queried index
 195              $diferences = array_merge(array_diff($columns, $indcolumns), array_diff($indcolumns, $columns));
 196              // If no differences, we have find the index
 197              if (empty($diferences)) {
 198                  if ($returnall) {
 199                      $return[] = $indexname;
 200                  } else {
 201                      return $indexname;
 202                  }
 203              }
 204          }
 205  
 206          if ($return and $returnall) {
 207              return $return;
 208          }
 209  
 210          // Arriving here, index not found
 211          return false;
 212      }
 213  
 214      /**
 215       * Given one xmldb_index, check if it exists in DB (true/false).
 216       *
 217       * @param xmldb_table $xmldb_table The table to be searched.
 218       * @param xmldb_index $xmldb_index The index to be searched for.
 219       * @return boolean true id index exists, false otherwise.
 220       */
 221      public function index_exists(xmldb_table $xmldb_table, xmldb_index $xmldb_index) {
 222          if (!$this->table_exists($xmldb_table)) {
 223              return false;
 224          }
 225          return ($this->find_index_name($xmldb_table, $xmldb_index) !== false);
 226      }
 227  
 228      /**
 229       * This function IS NOT IMPLEMENTED. ONCE WE'LL BE USING RELATIONAL
 230       * INTEGRITY IT WILL BECOME MORE USEFUL. FOR NOW, JUST CALCULATE "OFFICIAL"
 231       * KEY NAMES WITHOUT ACCESSING TO DB AT ALL.
 232       * Given one xmldb_key, the function returns the name of the key in DB (if exists)
 233       * of false if it doesn't exist
 234       *
 235       * @param xmldb_table $xmldb_table The table to be searched.
 236       * @param xmldb_key $xmldb_key The key to be searched.
 237       * @return string key name if found
 238       */
 239      public function find_key_name(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {
 240  
 241          $keycolumns = $xmldb_key->getFields();
 242  
 243          // Get list of keys in table
 244          // first primaries (we aren't going to use this now, because the MetaPrimaryKeys is awful)
 245              //TODO: To implement when we advance in relational integrity
 246          // then uniques (note that Moodle, for now, shouldn't have any UNIQUE KEY for now, but unique indexes)
 247              //TODO: To implement when we advance in relational integrity (note that AdoDB hasn't any MetaXXX for this.
 248          // then foreign (note that Moodle, for now, shouldn't have any FOREIGN KEY for now, but indexes)
 249              //TODO: To implement when we advance in relational integrity (note that AdoDB has one MetaForeignKeys()
 250              //but it's far from perfect.
 251          // TODO: To create the proper functions inside each generator to retrieve all the needed KEY info (name
 252          //       columns, reftable and refcolumns
 253  
 254          // So all we do is to return the official name of the requested key without any confirmation!)
 255          // One exception, hardcoded primary constraint names
 256          if ($this->generator->primary_key_name && $xmldb_key->getType() == XMLDB_KEY_PRIMARY) {
 257              return $this->generator->primary_key_name;
 258          } else {
 259              // Calculate the name suffix
 260              switch ($xmldb_key->getType()) {
 261                  case XMLDB_KEY_PRIMARY:
 262                      $suffix = 'pk';
 263                      break;
 264                  case XMLDB_KEY_UNIQUE:
 265                      $suffix = 'uk';
 266                      break;
 267                  case XMLDB_KEY_FOREIGN_UNIQUE:
 268                  case XMLDB_KEY_FOREIGN:
 269                      $suffix = 'fk';
 270                      break;
 271              }
 272              // And simply, return the official name
 273              return $this->generator->getNameForObject($xmldb_table->getName(), implode(', ', $xmldb_key->getFields()), $suffix);
 274          }
 275      }
 276  
 277      /**
 278       * This function will delete all tables found in XMLDB file from db
 279       *
 280       * @param string $file Full path to the XML file to be used.
 281       * @return void
 282       */
 283      public function delete_tables_from_xmldb_file($file) {
 284  
 285          $xmldb_file = new xmldb_file($file);
 286  
 287          if (!$xmldb_file->fileExists()) {
 288              throw new ddl_exception('ddlxmlfileerror', null, 'File does not exist');
 289          }
 290  
 291          $loaded    = $xmldb_file->loadXMLStructure();
 292          $structure = $xmldb_file->getStructure();
 293  
 294          if (!$loaded || !$xmldb_file->isLoaded()) {
 295              // Show info about the error if we can find it
 296              if ($structure) {
 297                  if ($errors = $structure->getAllErrors()) {
 298                      throw new ddl_exception('ddlxmlfileerror', null, 'Errors found in XMLDB file: '. implode (', ', $errors));
 299                  }
 300              }
 301              throw new ddl_exception('ddlxmlfileerror', null, 'not loaded??');
 302          }
 303  
 304          if ($xmldb_tables = $structure->getTables()) {
 305              // Delete in opposite order, this should help with foreign keys in the future.
 306              $xmldb_tables = array_reverse($xmldb_tables);
 307              foreach($xmldb_tables as $table) {
 308                  if ($this->table_exists($table)) {
 309                      $this->drop_table($table);
 310                  }
 311              }
 312          }
 313      }
 314  
 315      /**
 316       * This function will drop the table passed as argument
 317       * and all the associated objects (keys, indexes, constraints, sequences, triggers)
 318       * will be dropped too.
 319       *
 320       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 321       * @return void
 322       */
 323      public function drop_table(xmldb_table $xmldb_table) {
 324          // Check table exists
 325          if (!$this->table_exists($xmldb_table)) {
 326              throw new ddl_table_missing_exception($xmldb_table->getName());
 327          }
 328  
 329          if (!$sqlarr = $this->generator->getDropTableSQL($xmldb_table)) {
 330              throw new ddl_exception('ddlunknownerror', null, 'table drop sql not generated');
 331          }
 332          $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 333  
 334          $this->generator->cleanup_after_drop($xmldb_table);
 335      }
 336  
 337      /**
 338       * Load an install.xml file, checking that it exists, and that the structure is OK.
 339       * @param string $file the full path to the XMLDB file.
 340       * @return xmldb_file the loaded file.
 341       */
 342      private function load_xmldb_file($file) {
 343          $xmldb_file = new xmldb_file($file);
 344  
 345          if (!$xmldb_file->fileExists()) {
 346              throw new ddl_exception('ddlxmlfileerror', null, 'File does not exist');
 347          }
 348  
 349          $loaded = $xmldb_file->loadXMLStructure();
 350          if (!$loaded || !$xmldb_file->isLoaded()) {
 351              // Show info about the error if we can find it
 352              if ($structure = $xmldb_file->getStructure()) {
 353                  if ($errors = $structure->getAllErrors()) {
 354                      throw new ddl_exception('ddlxmlfileerror', null, 'Errors found in XMLDB file: '. implode (', ', $errors));
 355                  }
 356              }
 357              throw new ddl_exception('ddlxmlfileerror', null, 'not loaded??');
 358          }
 359  
 360          return $xmldb_file;
 361      }
 362  
 363      /**
 364       * This function will load one entire XMLDB file and call install_from_xmldb_structure.
 365       *
 366       * @param string $file full path to the XML file to be used
 367       * @return void
 368       */
 369      public function install_from_xmldb_file($file) {
 370          $xmldb_file = $this->load_xmldb_file($file);
 371          $xmldb_structure = $xmldb_file->getStructure();
 372          $this->install_from_xmldb_structure($xmldb_structure);
 373      }
 374  
 375      /**
 376       * This function will load one entire XMLDB file and call install_from_xmldb_structure.
 377       *
 378       * @param string $file full path to the XML file to be used
 379       * @param string $tablename the name of the table.
 380       * @param bool $cachestructures boolean to decide if loaded xmldb structures can be safely cached
 381       *             useful for testunits loading the enormous main xml file hundred of times (100x)
 382       */
 383      public function install_one_table_from_xmldb_file($file, $tablename, $cachestructures = false) {
 384  
 385          static $xmldbstructurecache = array(); // To store cached structures
 386          if (!empty($xmldbstructurecache) && array_key_exists($file, $xmldbstructurecache)) {
 387              $xmldb_structure = $xmldbstructurecache[$file];
 388          } else {
 389              $xmldb_file = $this->load_xmldb_file($file);
 390              $xmldb_structure = $xmldb_file->getStructure();
 391              if ($cachestructures) {
 392                  $xmldbstructurecache[$file] = $xmldb_structure;
 393              }
 394          }
 395  
 396          $targettable = $xmldb_structure->getTable($tablename);
 397          if (is_null($targettable)) {
 398              throw new ddl_exception('ddlunknowntable', null, 'The table ' . $tablename . ' is not defined in file ' . $file);
 399          }
 400          $targettable->setNext(NULL);
 401          $targettable->setPrevious(NULL);
 402  
 403          $tempstructure = new xmldb_structure('temp');
 404          $tempstructure->addTable($targettable);
 405          $this->install_from_xmldb_structure($tempstructure);
 406      }
 407  
 408      /**
 409       * This function will generate all the needed SQL statements, specific for each
 410       * RDBMS type and, finally, it will execute all those statements against the DB.
 411       *
 412       * @param stdClass $xmldb_structure xmldb_structure object.
 413       * @return void
 414       */
 415      public function install_from_xmldb_structure($xmldb_structure) {
 416  
 417          if (!$sqlarr = $this->generator->getCreateStructureSQL($xmldb_structure)) {
 418              return; // nothing to do
 419          }
 420  
 421          $tablenames = array();
 422          foreach ($xmldb_structure as $xmldb_table) {
 423              if ($xmldb_table instanceof xmldb_table) {
 424                  $tablenames[] = $xmldb_table->getName();
 425              }
 426          }
 427          $this->execute_sql_arr($sqlarr, $tablenames);
 428      }
 429  
 430      /**
 431       * This function will create the table passed as argument with all its
 432       * fields/keys/indexes/sequences, everything based in the XMLDB object
 433       *
 434       * @param xmldb_table $xmldb_table Table object (full specs are required).
 435       * @return void
 436       */
 437      public function create_table(xmldb_table $xmldb_table) {
 438          // Check table doesn't exist
 439          if ($this->table_exists($xmldb_table)) {
 440              throw new ddl_exception('ddltablealreadyexists', $xmldb_table->getName());
 441          }
 442  
 443          if (!$sqlarr = $this->generator->getCreateTableSQL($xmldb_table)) {
 444              throw new ddl_exception('ddlunknownerror', null, 'table create sql not generated');
 445          }
 446          $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 447      }
 448  
 449      /**
 450       * This function will create the temporary table passed as argument with all its
 451       * fields/keys/indexes/sequences, everything based in the XMLDB object
 452       *
 453       * If table already exists ddl_exception will be thrown, please make sure
 454       * the table name does not collide with existing normal table!
 455       *
 456       * @param xmldb_table $xmldb_table Table object (full specs are required).
 457       * @return void
 458       */
 459      public function create_temp_table(xmldb_table $xmldb_table) {
 460  
 461          // Check table doesn't exist
 462          if ($this->table_exists($xmldb_table)) {
 463              throw new ddl_exception('ddltablealreadyexists', $xmldb_table->getName());
 464          }
 465  
 466          if (!$sqlarr = $this->generator->getCreateTempTableSQL($xmldb_table)) {
 467              throw new ddl_exception('ddlunknownerror', null, 'temp table create sql not generated');
 468          }
 469          $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 470      }
 471  
 472      /**
 473       * This function will drop the temporary table passed as argument with all its
 474       * fields/keys/indexes/sequences, everything based in the XMLDB object
 475       *
 476       * It is recommended to drop temp table when not used anymore.
 477       *
 478       * @deprecated since 2.3, use drop_table() for all table types
 479       * @param xmldb_table $xmldb_table Table object.
 480       * @return void
 481       */
 482      public function drop_temp_table(xmldb_table $xmldb_table) {
 483          debugging('database_manager::drop_temp_table() is deprecated, use database_manager::drop_table() instead');
 484          $this->drop_table($xmldb_table);
 485      }
 486  
 487      /**
 488       * This function will rename the table passed as argument
 489       * Before renaming the index, the function will check it exists
 490       *
 491       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 492       * @param string $newname New name of the index.
 493       * @return void
 494       */
 495      public function rename_table(xmldb_table $xmldb_table, $newname) {
 496          // Check newname isn't empty
 497          if (!$newname) {
 498              throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
 499          }
 500  
 501          $check = new xmldb_table($newname);
 502  
 503          // Check table already renamed
 504          if (!$this->table_exists($xmldb_table)) {
 505              if ($this->table_exists($check)) {
 506                  throw new ddl_exception('ddlunknownerror', null, 'table probably already renamed');
 507              } else {
 508                  throw new ddl_table_missing_exception($xmldb_table->getName());
 509              }
 510          }
 511  
 512          // Check new table doesn't exist
 513          if ($this->table_exists($check)) {
 514              throw new ddl_exception('ddltablealreadyexists', $check->getName(), 'can not rename table');
 515          }
 516  
 517          if (!$sqlarr = $this->generator->getRenameTableSQL($xmldb_table, $newname)) {
 518              throw new ddl_exception('ddlunknownerror', null, 'table rename sql not generated');
 519          }
 520  
 521          $this->execute_sql_arr($sqlarr);
 522      }
 523  
 524      /**
 525       * This function will add the field to the table passed as arguments
 526       *
 527       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 528       * @param xmldb_field $xmldb_field Index object (full specs are required).
 529       * @return void
 530       */
 531      public function add_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
 532           // Check the field doesn't exist
 533          if ($this->field_exists($xmldb_table, $xmldb_field)) {
 534              throw new ddl_exception('ddlfieldalreadyexists', $xmldb_field->getName());
 535          }
 536  
 537          // If NOT NULL and no default given (we ask the generator about the
 538          // *real* default that will be used) check the table is empty
 539          if ($xmldb_field->getNotNull() && $this->generator->getDefaultValue($xmldb_field) === NULL && $this->mdb->count_records($xmldb_table->getName())) {
 540              throw new ddl_exception('ddlunknownerror', null, 'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .
 541                        ' cannot be added. Not null fields added to non empty tables require default value. Create skipped');
 542          }
 543  
 544          if (!$sqlarr = $this->generator->getAddFieldSQL($xmldb_table, $xmldb_field)) {
 545              throw new ddl_exception('ddlunknownerror', null, 'addfield sql not generated');
 546          }
 547          $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 548      }
 549  
 550      /**
 551       * This function will drop the field from the table passed as arguments
 552       *
 553       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 554       * @param xmldb_field $xmldb_field Index object (full specs are required).
 555       * @return void
 556       */
 557      public function drop_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
 558          if (!$this->table_exists($xmldb_table)) {
 559              throw new ddl_table_missing_exception($xmldb_table->getName());
 560          }
 561          // Check the field exists
 562          if (!$this->field_exists($xmldb_table, $xmldb_field)) {
 563              throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
 564          }
 565          // Check for dependencies in the DB before performing any action
 566          $this->check_field_dependencies($xmldb_table, $xmldb_field);
 567  
 568          if (!$sqlarr = $this->generator->getDropFieldSQL($xmldb_table, $xmldb_field)) {
 569              throw new ddl_exception('ddlunknownerror', null, 'drop_field sql not generated');
 570          }
 571  
 572          $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 573      }
 574  
 575      /**
 576       * This function will change the type of the field in the table passed as arguments
 577       *
 578       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 579       * @param xmldb_field $xmldb_field Index object (full specs are required).
 580       * @return void
 581       */
 582      public function change_field_type(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
 583          if (!$this->table_exists($xmldb_table)) {
 584              throw new ddl_table_missing_exception($xmldb_table->getName());
 585          }
 586          // Check the field exists
 587          if (!$this->field_exists($xmldb_table, $xmldb_field)) {
 588              throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
 589          }
 590          // Check for dependencies in the DB before performing any action
 591          $this->check_field_dependencies($xmldb_table, $xmldb_field);
 592  
 593          if (!$sqlarr = $this->generator->getAlterFieldSQL($xmldb_table, $xmldb_field)) {
 594              return; // probably nothing to do
 595          }
 596  
 597          $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 598      }
 599  
 600      /**
 601       * This function will change the precision of the field in the table passed as arguments
 602       *
 603       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 604       * @param xmldb_field $xmldb_field Index object (full specs are required).
 605       * @return void
 606       */
 607      public function change_field_precision(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
 608          // Just a wrapper over change_field_type. Does exactly the same processing
 609          $this->change_field_type($xmldb_table, $xmldb_field);
 610      }
 611  
 612      /**
 613       * This function will change the unsigned/signed of the field in the table passed as arguments
 614       *
 615       * @deprecated since 2.3, only singed numbers are allowed now, migration is automatic
 616       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 617       * @param xmldb_field $xmldb_field Field object (full specs are required).
 618       * @return void
 619       */
 620      public function change_field_unsigned(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
 621          debugging('All unsigned numbers are converted to signed automatically during Moodle upgrade.');
 622          $this->change_field_type($xmldb_table, $xmldb_field);
 623      }
 624  
 625      /**
 626       * This function will change the nullability of the field in the table passed as arguments
 627       *
 628       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 629       * @param xmldb_field $xmldb_field Index object (full specs are required).
 630       * @return void
 631       */
 632      public function change_field_notnull(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
 633          // Just a wrapper over change_field_type. Does exactly the same processing
 634          $this->change_field_type($xmldb_table, $xmldb_field);
 635      }
 636  
 637      /**
 638       * This function will change the default of the field in the table passed as arguments
 639       * One null value in the default field means delete the default
 640       *
 641       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 642       * @param xmldb_field $xmldb_field Index object (full specs are required).
 643       * @return void
 644       */
 645      public function change_field_default(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
 646          if (!$this->table_exists($xmldb_table)) {
 647              throw new ddl_table_missing_exception($xmldb_table->getName());
 648          }
 649          // Check the field exists
 650          if (!$this->field_exists($xmldb_table, $xmldb_field)) {
 651              throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
 652          }
 653          // Check for dependencies in the DB before performing any action
 654          $this->check_field_dependencies($xmldb_table, $xmldb_field);
 655  
 656          if (!$sqlarr = $this->generator->getModifyDefaultSQL($xmldb_table, $xmldb_field)) {
 657              return; //Empty array = nothing to do = no error
 658          }
 659  
 660          $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 661      }
 662  
 663      /**
 664       * This function will rename the field in the table passed as arguments
 665       * Before renaming the field, the function will check it exists
 666       *
 667       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 668       * @param xmldb_field $xmldb_field Index object (full specs are required).
 669       * @param string $newname New name of the field.
 670       * @return void
 671       */
 672      public function rename_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field, $newname) {
 673          if (empty($newname)) {
 674              throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
 675          }
 676  
 677          if (!$this->table_exists($xmldb_table)) {
 678              throw new ddl_table_missing_exception($xmldb_table->getName());
 679          }
 680  
 681          // Check the field exists
 682          if (!$this->field_exists($xmldb_table, $xmldb_field)) {
 683              throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
 684          }
 685  
 686          // Check we have included full field specs
 687          if (!$xmldb_field->getType()) {
 688              throw new ddl_exception('ddlunknownerror', null,
 689                        'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .
 690                        ' must contain full specs. Rename skipped');
 691          }
 692  
 693          // Check field isn't id. Renaming over that field is not allowed
 694          if ($xmldb_field->getName() == 'id') {
 695              throw new ddl_exception('ddlunknownerror', null,
 696                        'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .
 697                        ' cannot be renamed. Rename skipped');
 698          }
 699  
 700          if (!$sqlarr = $this->generator->getRenameFieldSQL($xmldb_table, $xmldb_field, $newname)) {
 701              return; //Empty array = nothing to do = no error
 702          }
 703  
 704          $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 705      }
 706  
 707      /**
 708       * This function will check, for the given table and field, if there there is any dependency
 709       * preventing the field to be modified. It's used by all the public methods that perform any
 710       * DDL change on fields, throwing one ddl_dependency_exception if dependencies are found.
 711       *
 712       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 713       * @param xmldb_field $xmldb_field Index object (full specs are required).
 714       * @return void
 715       * @throws ddl_dependency_exception|ddl_field_missing_exception|ddl_table_missing_exception if dependency not met.
 716       */
 717      private function check_field_dependencies(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
 718  
 719          // Check the table exists
 720          if (!$this->table_exists($xmldb_table)) {
 721              throw new ddl_table_missing_exception($xmldb_table->getName());
 722          }
 723  
 724          // Check the field exists
 725          if (!$this->field_exists($xmldb_table, $xmldb_field)) {
 726              throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
 727          }
 728  
 729          // Check the field isn't in use by any index in the table
 730          if ($indexes = $this->mdb->get_indexes($xmldb_table->getName(), false)) {
 731              foreach ($indexes as $indexname => $index) {
 732                  $columns = $index['columns'];
 733                  if (in_array($xmldb_field->getName(), $columns)) {
 734                      throw new ddl_dependency_exception('column', $xmldb_table->getName() . '->' . $xmldb_field->getName(),
 735                                                         'index', $indexname . ' (' . implode(', ', $columns)  . ')');
 736                  }
 737              }
 738          }
 739      }
 740  
 741      /**
 742       * This function will create the key in the table passed as arguments
 743       *
 744       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 745       * @param xmldb_key $xmldb_key Index object (full specs are required).
 746       * @return void
 747       */
 748      public function add_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {
 749  
 750          if ($xmldb_key->getType() == XMLDB_KEY_PRIMARY) { // Prevent PRIMARY to be added (only in create table, being serious  :-P)
 751              throw new ddl_exception('ddlunknownerror', null, 'Primary Keys can be added at table create time only');
 752          }
 753  
 754          if (!$sqlarr = $this->generator->getAddKeySQL($xmldb_table, $xmldb_key)) {
 755              return; //Empty array = nothing to do = no error
 756          }
 757  
 758          $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 759      }
 760  
 761      /**
 762       * This function will drop the key in the table passed as arguments
 763       *
 764       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 765       * @param xmldb_key $xmldb_key Key object (full specs are required).
 766       * @return void
 767       */
 768      public function drop_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {
 769          if ($xmldb_key->getType() == XMLDB_KEY_PRIMARY) { // Prevent PRIMARY to be dropped (only in drop table, being serious  :-P)
 770              throw new ddl_exception('ddlunknownerror', null, 'Primary Keys can be deleted at table drop time only');
 771          }
 772  
 773          if (!$sqlarr = $this->generator->getDropKeySQL($xmldb_table, $xmldb_key)) {
 774              return; //Empty array = nothing to do = no error
 775          }
 776  
 777          $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 778      }
 779  
 780      /**
 781       * This function will rename the key in the table passed as arguments
 782       * Experimental. Shouldn't be used at all in normal installation/upgrade!
 783       *
 784       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 785       * @param xmldb_key $xmldb_key key object (full specs are required).
 786       * @param string $newname New name of the key.
 787       * @return void
 788       */
 789      public function rename_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key, $newname) {
 790          debugging('rename_key() is one experimental feature. You must not use it in production!', DEBUG_DEVELOPER);
 791  
 792          // Check newname isn't empty
 793          if (!$newname) {
 794              throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
 795          }
 796  
 797          if (!$sqlarr = $this->generator->getRenameKeySQL($xmldb_table, $xmldb_key, $newname)) {
 798              throw new ddl_exception('ddlunknownerror', null, 'Some DBs do not support key renaming (MySQL, PostgreSQL, MsSQL). Rename skipped');
 799          }
 800  
 801          $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 802      }
 803  
 804      /**
 805       * This function will create the index in the table passed as arguments
 806       * Before creating the index, the function will check it doesn't exists
 807       *
 808       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 809       * @param xmldb_index $xmldb_intex Index object (full specs are required).
 810       * @return void
 811       */
 812      public function add_index($xmldb_table, $xmldb_intex) {
 813          if (!$this->table_exists($xmldb_table)) {
 814              throw new ddl_table_missing_exception($xmldb_table->getName());
 815          }
 816  
 817          // Check index doesn't exist
 818          if ($this->index_exists($xmldb_table, $xmldb_intex)) {
 819              throw new ddl_exception('ddlunknownerror', null,
 820                        'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .
 821                        ' already exists. Create skipped');
 822          }
 823  
 824          if (!$sqlarr = $this->generator->getAddIndexSQL($xmldb_table, $xmldb_intex)) {
 825              throw new ddl_exception('ddlunknownerror', null, 'add_index sql not generated');
 826          }
 827  
 828          try {
 829              $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 830          } catch (ddl_change_structure_exception $e) {
 831              // There could be a problem with the index length related to the row format of the table.
 832              // If we are using utf8mb4 and the row format is 'compact' or 'redundant' then we need to change it over to
 833              // 'compressed' or 'dynamic'.
 834              if (method_exists($this->mdb, 'convert_table_row_format')) {
 835                  $this->mdb->convert_table_row_format($xmldb_table->getName());
 836                  $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 837              } else {
 838                  // It's some other problem that we are currently not handling.
 839                  throw $e;
 840              }
 841          }
 842      }
 843  
 844      /**
 845       * This function will drop the index in the table passed as arguments
 846       * Before dropping the index, the function will check it exists
 847       *
 848       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 849       * @param xmldb_index $xmldb_intex Index object (full specs are required).
 850       * @return void
 851       */
 852      public function drop_index($xmldb_table, $xmldb_intex) {
 853          if (!$this->table_exists($xmldb_table)) {
 854              throw new ddl_table_missing_exception($xmldb_table->getName());
 855          }
 856  
 857          // Check index exists
 858          if (!$this->index_exists($xmldb_table, $xmldb_intex)) {
 859              throw new ddl_exception('ddlunknownerror', null,
 860                        'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .
 861                        ' does not exist. Drop skipped');
 862          }
 863  
 864          if (!$sqlarr = $this->generator->getDropIndexSQL($xmldb_table, $xmldb_intex)) {
 865              throw new ddl_exception('ddlunknownerror', null, 'drop_index sql not generated');
 866          }
 867  
 868          $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 869      }
 870  
 871      /**
 872       * This function will rename the index in the table passed as arguments
 873       * Before renaming the index, the function will check it exists
 874       * Experimental. Shouldn't be used at all!
 875       *
 876       * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
 877       * @param xmldb_index $xmldb_intex Index object (full specs are required).
 878       * @param string $newname New name of the index.
 879       * @return void
 880       */
 881      public function rename_index($xmldb_table, $xmldb_intex, $newname) {
 882          debugging('rename_index() is one experimental feature. You must not use it in production!', DEBUG_DEVELOPER);
 883  
 884          // Check newname isn't empty
 885          if (!$newname) {
 886              throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
 887          }
 888  
 889          // Check index exists
 890          if (!$this->index_exists($xmldb_table, $xmldb_intex)) {
 891              throw new ddl_exception('ddlunknownerror', null,
 892                        'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .
 893                        ' does not exist. Rename skipped');
 894          }
 895  
 896          if (!$sqlarr = $this->generator->getRenameIndexSQL($xmldb_table, $xmldb_intex, $newname)) {
 897              throw new ddl_exception('ddlunknownerror', null, 'Some DBs do not support index renaming (MySQL). Rename skipped');
 898          }
 899  
 900          $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
 901      }
 902  
 903      /**
 904       * Get the list of install.xml files.
 905       *
 906       * @return array
 907       */
 908      public function get_install_xml_files(): array {
 909          global $CFG;
 910          require_once($CFG->libdir.'/adminlib.php');
 911  
 912          $files = [];
 913          $dbdirs = get_db_directories();
 914          foreach ($dbdirs as $dbdir) {
 915              $filename = "{$dbdir}/install.xml";
 916              if (file_exists($filename)) {
 917                  $files[] = $filename;
 918              }
 919          }
 920  
 921          return $files;
 922      }
 923  
 924      /**
 925       * Reads the install.xml files for Moodle core and modules and returns an array of
 926       * xmldb_structure object with xmldb_table from these files.
 927       * @return xmldb_structure schema from install.xml files
 928       */
 929      public function get_install_xml_schema() {
 930          global $CFG;
 931          require_once($CFG->libdir.'/adminlib.php');
 932  
 933          $schema = new xmldb_structure('export');
 934          $schema->setVersion($CFG->version);
 935  
 936          foreach ($this->get_install_xml_files() as $filename) {
 937              $xmldb_file = new xmldb_file($filename);
 938              if (!$xmldb_file->loadXMLStructure()) {
 939                  continue;
 940              }
 941              $structure = $xmldb_file->getStructure();
 942              $tables = $structure->getTables();
 943              foreach ($tables as $table) {
 944                  $table->setPrevious(null);
 945                  $table->setNext(null);
 946                  $schema->addTable($table);
 947              }
 948          }
 949          return $schema;
 950      }
 951  
 952      /**
 953       * Checks the database schema against a schema specified by an xmldb_structure object
 954       * @param xmldb_structure $schema export schema describing all known tables
 955       * @param array $options
 956       * @return array keyed by table name with array of difference messages as values
 957       */
 958      public function check_database_schema(xmldb_structure $schema, array $options = null) {
 959          $alloptions = array(
 960              'extratables' => true,
 961              'missingtables' => true,
 962              'extracolumns' => true,
 963              'missingcolumns' => true,
 964              'changedcolumns' => true,
 965              'missingindexes' => true,
 966              'extraindexes' => true
 967          );
 968  
 969          $typesmap = array(
 970              'I' => XMLDB_TYPE_INTEGER,
 971              'R' => XMLDB_TYPE_INTEGER,
 972              'N' => XMLDB_TYPE_NUMBER,
 973              'F' => XMLDB_TYPE_NUMBER, // Nobody should be using floats!
 974              'C' => XMLDB_TYPE_CHAR,
 975              'X' => XMLDB_TYPE_TEXT,
 976              'B' => XMLDB_TYPE_BINARY,
 977              'T' => XMLDB_TYPE_TIMESTAMP,
 978              'D' => XMLDB_TYPE_DATETIME,
 979          );
 980  
 981          $options = (array)$options;
 982          $options = array_merge($alloptions, $options);
 983  
 984          // Note: the error descriptions are not supposed to be localised,
 985          //       it is intended for developers and skilled admins only.
 986          $errors = array();
 987  
 988          /** @var string[] $dbtables */
 989          $dbtables = $this->mdb->get_tables(false);
 990          /** @var xmldb_table[] $tables */
 991          $tables = $schema->getTables();
 992  
 993          foreach ($tables as $table) {
 994              $tablename = $table->getName();
 995  
 996              if ($options['missingtables']) {
 997                  // Missing tables are a fatal problem.
 998                  if (empty($dbtables[$tablename])) {
 999                      $errors[$tablename][] = "table is missing";
1000                      continue;
1001                  }
1002              }
1003  
1004              /** @var database_column_info[] $dbfields */
1005              $dbfields = $this->mdb->get_columns($tablename, false);
1006              $dbindexes = $this->mdb->get_indexes($tablename);
1007              /** @var xmldb_field[] $fields */
1008              $fields = $table->getFields();
1009  
1010              foreach ($fields as $field) {
1011                  $fieldname = $field->getName();
1012                  if (empty($dbfields[$fieldname])) {
1013                      if ($options['missingcolumns']) {
1014                          // Missing columns are a fatal problem.
1015                          $errors[$tablename][] = "column '$fieldname' is missing";
1016                      }
1017                  } else if ($options['changedcolumns']) {
1018                      $dbfield = $dbfields[$fieldname];
1019  
1020                      if (!isset($typesmap[$dbfield->meta_type])) {
1021                          $errors[$tablename][] = "column '$fieldname' has unsupported type '$dbfield->meta_type'";
1022                      } else {
1023                          $dbtype = $typesmap[$dbfield->meta_type];
1024                          $type = $field->getType();
1025                          if ($type == XMLDB_TYPE_FLOAT) {
1026                              $type = XMLDB_TYPE_NUMBER;
1027                          }
1028                          if ($type != $dbtype) {
1029                              if ($expected = array_search($type, $typesmap)) {
1030                                  $errors[$tablename][] = "column '$fieldname' has incorrect type '$dbfield->meta_type', expected '$expected'";
1031                              } else {
1032                                  $errors[$tablename][] = "column '$fieldname' has incorrect type '$dbfield->meta_type'";
1033                              }
1034                          } else {
1035                              if ($field->getNotNull() != $dbfield->not_null) {
1036                                  if ($field->getNotNull()) {
1037                                      $errors[$tablename][] = "column '$fieldname' should be NOT NULL ($dbfield->meta_type)";
1038                                  } else {
1039                                      $errors[$tablename][] = "column '$fieldname' should allow NULL ($dbfield->meta_type)";
1040                                  }
1041                              }
1042                              if ($dbtype == XMLDB_TYPE_TEXT) {
1043                                  // No length check necessary - there is one size only now.
1044  
1045                              } else if ($dbtype == XMLDB_TYPE_NUMBER) {
1046                                  if ($field->getType() == XMLDB_TYPE_FLOAT) {
1047                                      // Do not use floats in any new code, they are deprecated in XMLDB editor!
1048  
1049                                  } else if ($field->getLength() != $dbfield->max_length or $field->getDecimals() != $dbfield->scale) {
1050                                      $size = "({$field->getLength()},{$field->getDecimals()})";
1051                                      $dbsize = "($dbfield->max_length,$dbfield->scale)";
1052                                      $errors[$tablename][] = "column '$fieldname' size is $dbsize, expected $size ($dbfield->meta_type)";
1053                                  }
1054  
1055                              } else if ($dbtype == XMLDB_TYPE_CHAR) {
1056                                  // This is not critical, but they should ideally match.
1057                                  if ($field->getLength() != $dbfield->max_length) {
1058                                      $errors[$tablename][] = "column '$fieldname' length is $dbfield->max_length, expected {$field->getLength()} ($dbfield->meta_type)";
1059                                  }
1060  
1061                              } else if ($dbtype == XMLDB_TYPE_INTEGER) {
1062                                  // Integers may be bigger in some DBs.
1063                                  $length = $field->getLength();
1064                                  if ($length > 18) {
1065                                      // Integers are not supposed to be bigger than 18.
1066                                      $length = 18;
1067                                  }
1068                                  if ($length > $dbfield->max_length) {
1069                                      $errors[$tablename][] = "column '$fieldname' length is $dbfield->max_length, expected at least {$field->getLength()} ($dbfield->meta_type)";
1070                                  }
1071  
1072                              } else if ($dbtype == XMLDB_TYPE_BINARY) {
1073                                  // Ignore binary types.
1074                                  continue;
1075  
1076                              } else if ($dbtype == XMLDB_TYPE_TIMESTAMP) {
1077                                  $errors[$tablename][] = "column '$fieldname' is a timestamp, this type is not supported ($dbfield->meta_type)";
1078                                  continue;
1079  
1080                              } else if ($dbtype == XMLDB_TYPE_DATETIME) {
1081                                  $errors[$tablename][] = "column '$fieldname' is a datetime, this type is not supported ($dbfield->meta_type)";
1082                                  continue;
1083  
1084                              } else {
1085                                  // Report all other unsupported types as problems.
1086                                  $errors[$tablename][] = "column '$fieldname' has unknown type ($dbfield->meta_type)";
1087                                  continue;
1088                              }
1089  
1090                              // Note: The empty string defaults are a bit messy...
1091                              if ($field->getDefault() != $dbfield->default_value) {
1092                                  $default = is_null($field->getDefault()) ? 'NULL' : $field->getDefault();
1093                                  $dbdefault = is_null($dbfield->default_value) ? 'NULL' : $dbfield->default_value;
1094                                  $errors[$tablename][] = "column '$fieldname' has default '$dbdefault', expected '$default' ($dbfield->meta_type)";
1095                              }
1096                          }
1097                      }
1098                  }
1099                  unset($dbfields[$fieldname]);
1100              }
1101  
1102              // Check for missing indexes/keys.
1103              if ($options['missingindexes']) {
1104                  // Check the foreign keys.
1105                  if ($keys = $table->getKeys()) {
1106                      foreach ($keys as $key) {
1107                          // Primary keys are skipped.
1108                          if ($key->getType() == XMLDB_KEY_PRIMARY) {
1109                              continue;
1110                          }
1111  
1112                          $keyname = $key->getName();
1113  
1114                          // Create the interim index.
1115                          $index = new xmldb_index('anyname');
1116                          $index->setFields($key->getFields());
1117                          switch ($key->getType()) {
1118                              case XMLDB_KEY_UNIQUE:
1119                              case XMLDB_KEY_FOREIGN_UNIQUE:
1120                                  $index->setUnique(true);
1121                                  break;
1122                              case XMLDB_KEY_FOREIGN:
1123                                  $index->setUnique(false);
1124                                  break;
1125                          }
1126                          if (!$this->index_exists($table, $index)) {
1127                              $errors[$tablename][] = $this->get_missing_index_error($table, $index, $keyname);
1128                          } else {
1129                              $this->remove_index_from_dbindex($dbindexes, $index);
1130                          }
1131                      }
1132                  }
1133  
1134                  // Check the indexes.
1135                  if ($indexes = $table->getIndexes()) {
1136                      foreach ($indexes as $index) {
1137                          if (!$this->index_exists($table, $index)) {
1138                              $errors[$tablename][] = $this->get_missing_index_error($table, $index, $index->getName());
1139                          } else {
1140                              $this->remove_index_from_dbindex($dbindexes, $index);
1141                          }
1142                      }
1143                  }
1144              }
1145  
1146              // Check if we should show the extra indexes.
1147              if ($options['extraindexes']) {
1148                  // Hack - skip for table 'search_simpledb_index' as this plugin adds indexes dynamically on install
1149                  // which are not included in install.xml. See search/engine/simpledb/db/install.php.
1150                  if ($tablename != 'search_simpledb_index') {
1151                      foreach ($dbindexes as $indexname => $index) {
1152                          $errors[$tablename][] = "Unexpected index '$indexname'.";
1153                      }
1154                  }
1155              }
1156  
1157              // Check for extra columns (indicates unsupported hacks) - modify install.xml if you want to pass validation.
1158              foreach ($dbfields as $fieldname => $dbfield) {
1159                  if ($options['extracolumns']) {
1160                      $errors[$tablename][] = "column '$fieldname' is not expected ($dbfield->meta_type)";
1161                  }
1162              }
1163              unset($dbtables[$tablename]);
1164          }
1165  
1166          if ($options['extratables']) {
1167              // Look for unsupported tables - local custom tables should be in /local/xxxx/db/install.xml file.
1168              // If there is no prefix, we can not say if table is ours, sorry.
1169              if ($this->generator->prefix !== '') {
1170                  foreach ($dbtables as $tablename => $unused) {
1171                      if (strpos($tablename, 'pma_') === 0) {
1172                          // Ignore phpmyadmin tables.
1173                          continue;
1174                      }
1175                      if (strpos($tablename, 'test') === 0) {
1176                          // Legacy simple test db tables need to be eventually removed,
1177                          // report them as problems!
1178                          $errors[$tablename][] = "table is not expected (it may be a leftover after Simpletest unit tests)";
1179                      } else {
1180                          $errors[$tablename][] = "table is not expected";
1181                      }
1182                  }
1183              }
1184          }
1185  
1186          return $errors;
1187      }
1188  
1189      /**
1190       * Returns a string describing the missing index error.
1191       *
1192       * @param xmldb_table $table
1193       * @param xmldb_index $index
1194       * @param string $indexname
1195       * @return string
1196       */
1197      private function get_missing_index_error(xmldb_table $table, xmldb_index $index, string $indexname): string {
1198          $sqlarr = $this->generator->getAddIndexSQL($table, $index);
1199          $sqlarr = $this->generator->getEndedStatements($sqlarr);
1200          $sqltoadd = reset($sqlarr);
1201  
1202          return "Missing index '" . $indexname . "' " . "(" . $index->readableInfo() . "). \n" . $sqltoadd;
1203      }
1204  
1205      /**
1206       * Removes an index from the array $dbindexes if it is found.
1207       *
1208       * @param array $dbindexes
1209       * @param xmldb_index $index
1210       */
1211      private function remove_index_from_dbindex(array &$dbindexes, xmldb_index $index) {
1212          foreach ($dbindexes as $key => $dbindex) {
1213              if ($dbindex['columns'] == $index->getFields()) {
1214                  unset($dbindexes[$key]);
1215              }
1216          }
1217      }
1218  }