Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Database manager instance is responsible for all database structure modifications.** @package core_ddl* @copyright 1999 onwards Martin Dougiamas http://dougiamas.com* 2001-3001 Eloy Lafuente (stronk7) http://contiento.com* 2008 Petr Skoda http://skodak.org* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/defined('MOODLE_INTERNAL') || die();/*** Database manager instance is responsible for all database structure modifications.** It is using db specific generators to find out the correct SQL syntax to do that.** @package core_ddl* @copyright 1999 onwards Martin Dougiamas http://dougiamas.com* 2001-3001 Eloy Lafuente (stronk7) http://contiento.com* 2008 Petr Skoda http://skodak.org* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class database_manager {/** @var moodle_database A moodle_database driver specific instance.*/protected $mdb;/** @var sql_generator A driver specific SQL generator instance. Public because XMLDB editor needs to access it.*/public $generator;/*** Creates a new database manager instance.* @param moodle_database $mdb A moodle_database driver specific instance.* @param sql_generator $generator A driver specific SQL generator instance.*/public function __construct($mdb, $generator) {$this->mdb = $mdb;$this->generator = $generator;}/*** Releases all resources*/public function dispose() {if ($this->generator) {$this->generator->dispose();$this->generator = null;}$this->mdb = null;}/*** This function will execute an array of SQL commands.** @param string[] $sqlarr Array of sql statements to execute.* @param array|null $tablenames an array of xmldb table names affected by this request.* @throws ddl_change_structure_exception This exception is thrown if any error is found.*/protected function execute_sql_arr(array $sqlarr, $tablenames = null) {$this->mdb->change_database_structure($sqlarr, $tablenames);}/*** Execute a given sql command string.** @param string $sql The sql string you wish to be executed.* @throws ddl_change_structure_exception This exception is thrown if any error is found.*/protected function execute_sql($sql) {$this->mdb->change_database_structure($sql);}/*** Given one xmldb_table, check if it exists in DB (true/false).** @param string|xmldb_table $table The table to be searched (string name or xmldb_table instance).* @return bool True is a table exists, false otherwise.*/public function table_exists($table) {if (!is_string($table) and !($table instanceof xmldb_table)) {throw new ddl_exception('ddlunknownerror', NULL, 'incorrect table parameter!');}return $this->generator->table_exists($table);}/*** Reset a sequence to the id field of a table.* @param string|xmldb_table $table Name of table.* @throws ddl_exception thrown upon reset errors.*/public function reset_sequence($table) {if (!is_string($table) and !($table instanceof xmldb_table)) {throw new ddl_exception('ddlunknownerror', NULL, 'incorrect table parameter!');} else {if ($table instanceof xmldb_table) {$tablename = $table->getName();} else {$tablename = $table;}}// Do not test if table exists because it is slowif (!$sqlarr = $this->generator->getResetSequenceSQL($table)) {throw new ddl_exception('ddlunknownerror', null, 'table reset sequence sql not generated');}$this->execute_sql_arr($sqlarr, array($tablename));}/*** Given one xmldb_field, check if it exists in DB (true/false).** @param string|xmldb_table $table The table to be searched (string name or xmldb_table instance).* @param string|xmldb_field $field The field to be searched for (string name or xmldb_field instance).* @return boolean true is exists false otherwise.* @throws ddl_table_missing_exception*/public function field_exists($table, $field) {// Calculate the name of the tableif (is_string($table)) {$tablename = $table;} else {$tablename = $table->getName();}// Check the table existsif (!$this->table_exists($table)) {throw new ddl_table_missing_exception($tablename);}if (is_string($field)) {$fieldname = $field;} else {// Calculate the name of the table$fieldname = $field->getName();}// Get list of fields in table$columns = $this->mdb->get_columns($tablename);$exists = array_key_exists($fieldname, $columns);return $exists;}/*** Given one xmldb_index, the function returns the name of the index in DB* of false if it doesn't exist** @param xmldb_table $xmldb_table table to be searched* @param xmldb_index $xmldb_index the index to be searched* @param bool $returnall true means return array of all indexes, false means first index only as string* @return array|string|bool Index name, array of index names or false if no indexes are found.* @throws ddl_table_missing_exception Thrown when table is not found.*/public function find_index_name(xmldb_table $xmldb_table, xmldb_index $xmldb_index, $returnall = false) {// Calculate the name of the table$tablename = $xmldb_table->getName();// Check the table existsif (!$this->table_exists($xmldb_table)) {throw new ddl_table_missing_exception($tablename);}// Extract index columns$indcolumns = $xmldb_index->getFields();// Get list of indexes in table$indexes = $this->mdb->get_indexes($tablename);$return = array();// Iterate over them looking for columns coincidenceforeach ($indexes as $indexname => $index) {$columns = $index['columns'];// Check if index matches queried index$diferences = array_merge(array_diff($columns, $indcolumns), array_diff($indcolumns, $columns));// If no differences, we have find the indexif (empty($diferences)) {if ($returnall) {$return[] = $indexname;} else {return $indexname;}}}if ($return and $returnall) {return $return;}// Arriving here, index not foundreturn false;}/*** Given one xmldb_index, check if it exists in DB (true/false).** @param xmldb_table $xmldb_table The table to be searched.* @param xmldb_index $xmldb_index The index to be searched for.* @return boolean true id index exists, false otherwise.*/public function index_exists(xmldb_table $xmldb_table, xmldb_index $xmldb_index) {if (!$this->table_exists($xmldb_table)) {return false;}return ($this->find_index_name($xmldb_table, $xmldb_index) !== false);}/*** This function IS NOT IMPLEMENTED. ONCE WE'LL BE USING RELATIONAL* INTEGRITY IT WILL BECOME MORE USEFUL. FOR NOW, JUST CALCULATE "OFFICIAL"* KEY NAMES WITHOUT ACCESSING TO DB AT ALL.* Given one xmldb_key, the function returns the name of the key in DB (if exists)* of false if it doesn't exist** @param xmldb_table $xmldb_table The table to be searched.* @param xmldb_key $xmldb_key The key to be searched.* @return string key name if found*/public function find_key_name(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {$keycolumns = $xmldb_key->getFields();// Get list of keys in table// first primaries (we aren't going to use this now, because the MetaPrimaryKeys is awful)//TODO: To implement when we advance in relational integrity// then uniques (note that Moodle, for now, shouldn't have any UNIQUE KEY for now, but unique indexes)//TODO: To implement when we advance in relational integrity (note that AdoDB hasn't any MetaXXX for this.// then foreign (note that Moodle, for now, shouldn't have any FOREIGN KEY for now, but indexes)//TODO: To implement when we advance in relational integrity (note that AdoDB has one MetaForeignKeys()//but it's far from perfect.// TODO: To create the proper functions inside each generator to retrieve all the needed KEY info (name// columns, reftable and refcolumns// So all we do is to return the official name of the requested key without any confirmation!)// One exception, hardcoded primary constraint namesif ($this->generator->primary_key_name && $xmldb_key->getType() == XMLDB_KEY_PRIMARY) {return $this->generator->primary_key_name;} else {// Calculate the name suffixswitch ($xmldb_key->getType()) {case XMLDB_KEY_PRIMARY:$suffix = 'pk';break;case XMLDB_KEY_UNIQUE:$suffix = 'uk';break;case XMLDB_KEY_FOREIGN_UNIQUE:case XMLDB_KEY_FOREIGN:$suffix = 'fk';break;}// And simply, return the official namereturn $this->generator->getNameForObject($xmldb_table->getName(), implode(', ', $xmldb_key->getFields()), $suffix);}}/*** This function will delete all tables found in XMLDB file from db** @param string $file Full path to the XML file to be used.* @return void*/public function delete_tables_from_xmldb_file($file) {$xmldb_file = new xmldb_file($file);if (!$xmldb_file->fileExists()) {throw new ddl_exception('ddlxmlfileerror', null, 'File does not exist');}$loaded = $xmldb_file->loadXMLStructure();$structure = $xmldb_file->getStructure();if (!$loaded || !$xmldb_file->isLoaded()) {// Show info about the error if we can find itif ($structure) {if ($errors = $structure->getAllErrors()) {throw new ddl_exception('ddlxmlfileerror', null, 'Errors found in XMLDB file: '. implode (', ', $errors));}}throw new ddl_exception('ddlxmlfileerror', null, 'not loaded??');}if ($xmldb_tables = $structure->getTables()) {// Delete in opposite order, this should help with foreign keys in the future.$xmldb_tables = array_reverse($xmldb_tables);foreach($xmldb_tables as $table) {if ($this->table_exists($table)) {$this->drop_table($table);}}}}/*** This function will drop the table passed as argument* and all the associated objects (keys, indexes, constraints, sequences, triggers)* will be dropped too.** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @return void*/public function drop_table(xmldb_table $xmldb_table) {// Check table existsif (!$this->table_exists($xmldb_table)) {throw new ddl_table_missing_exception($xmldb_table->getName());}if (!$sqlarr = $this->generator->getDropTableSQL($xmldb_table)) {throw new ddl_exception('ddlunknownerror', null, 'table drop sql not generated');}$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));$this->generator->cleanup_after_drop($xmldb_table);}/*** Load an install.xml file, checking that it exists, and that the structure is OK.* @param string $file the full path to the XMLDB file.* @return xmldb_file the loaded file.*/private function load_xmldb_file($file) {$xmldb_file = new xmldb_file($file);if (!$xmldb_file->fileExists()) {throw new ddl_exception('ddlxmlfileerror', null, 'File does not exist');}$loaded = $xmldb_file->loadXMLStructure();if (!$loaded || !$xmldb_file->isLoaded()) {// Show info about the error if we can find itif ($structure = $xmldb_file->getStructure()) {if ($errors = $structure->getAllErrors()) {throw new ddl_exception('ddlxmlfileerror', null, 'Errors found in XMLDB file: '. implode (', ', $errors));}}throw new ddl_exception('ddlxmlfileerror', null, 'not loaded??');}return $xmldb_file;}/*** This function will load one entire XMLDB file and call install_from_xmldb_structure.** @param string $file full path to the XML file to be used* @return void*/public function install_from_xmldb_file($file) {$xmldb_file = $this->load_xmldb_file($file);$xmldb_structure = $xmldb_file->getStructure();$this->install_from_xmldb_structure($xmldb_structure);}/*** This function will load one entire XMLDB file and call install_from_xmldb_structure.** @param string $file full path to the XML file to be used* @param string $tablename the name of the table.* @param bool $cachestructures boolean to decide if loaded xmldb structures can be safely cached* useful for testunits loading the enormous main xml file hundred of times (100x)*/public function install_one_table_from_xmldb_file($file, $tablename, $cachestructures = false) {static $xmldbstructurecache = array(); // To store cached structuresif (!empty($xmldbstructurecache) && array_key_exists($file, $xmldbstructurecache)) {$xmldb_structure = $xmldbstructurecache[$file];} else {$xmldb_file = $this->load_xmldb_file($file);$xmldb_structure = $xmldb_file->getStructure();if ($cachestructures) {$xmldbstructurecache[$file] = $xmldb_structure;}}$targettable = $xmldb_structure->getTable($tablename);if (is_null($targettable)) {throw new ddl_exception('ddlunknowntable', null, 'The table ' . $tablename . ' is not defined in file ' . $file);}$targettable->setNext(NULL);$targettable->setPrevious(NULL);$tempstructure = new xmldb_structure('temp');$tempstructure->addTable($targettable);$this->install_from_xmldb_structure($tempstructure);}/*** This function will generate all the needed SQL statements, specific for each* RDBMS type and, finally, it will execute all those statements against the DB.** @param stdClass $xmldb_structure xmldb_structure object.* @return void*/public function install_from_xmldb_structure($xmldb_structure) {if (!$sqlarr = $this->generator->getCreateStructureSQL($xmldb_structure)) {return; // nothing to do}$tablenames = array();foreach ($xmldb_structure as $xmldb_table) {if ($xmldb_table instanceof xmldb_table) {$tablenames[] = $xmldb_table->getName();}}$this->execute_sql_arr($sqlarr, $tablenames);}/*** This function will create the table passed as argument with all its* fields/keys/indexes/sequences, everything based in the XMLDB object** @param xmldb_table $xmldb_table Table object (full specs are required).* @return void*/public function create_table(xmldb_table $xmldb_table) {// Check table doesn't existif ($this->table_exists($xmldb_table)) {throw new ddl_exception('ddltablealreadyexists', $xmldb_table->getName());}if (!$sqlarr = $this->generator->getCreateTableSQL($xmldb_table)) {throw new ddl_exception('ddlunknownerror', null, 'table create sql not generated');}$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));}/*** This function will create the temporary table passed as argument with all its* fields/keys/indexes/sequences, everything based in the XMLDB object** If table already exists ddl_exception will be thrown, please make sure* the table name does not collide with existing normal table!** @param xmldb_table $xmldb_table Table object (full specs are required).* @return void*/public function create_temp_table(xmldb_table $xmldb_table) {// Check table doesn't existif ($this->table_exists($xmldb_table)) {throw new ddl_exception('ddltablealreadyexists', $xmldb_table->getName());}if (!$sqlarr = $this->generator->getCreateTempTableSQL($xmldb_table)) {throw new ddl_exception('ddlunknownerror', null, 'temp table create sql not generated');}$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));}/*** This function will drop the temporary table passed as argument with all its* fields/keys/indexes/sequences, everything based in the XMLDB object** It is recommended to drop temp table when not used anymore.** @deprecated since 2.3, use drop_table() for all table types* @param xmldb_table $xmldb_table Table object.* @return void*/public function drop_temp_table(xmldb_table $xmldb_table) {debugging('database_manager::drop_temp_table() is deprecated, use database_manager::drop_table() instead');$this->drop_table($xmldb_table);}/*** This function will rename the table passed as argument* Before renaming the index, the function will check it exists** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param string $newname New name of the index.* @return void*/public function rename_table(xmldb_table $xmldb_table, $newname) {// Check newname isn't emptyif (!$newname) {throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');}$check = new xmldb_table($newname);// Check table already renamedif (!$this->table_exists($xmldb_table)) {if ($this->table_exists($check)) {throw new ddl_exception('ddlunknownerror', null, 'table probably already renamed');} else {throw new ddl_table_missing_exception($xmldb_table->getName());}}// Check new table doesn't existif ($this->table_exists($check)) {throw new ddl_exception('ddltablealreadyexists', $check->getName(), 'can not rename table');}if (!$sqlarr = $this->generator->getRenameTableSQL($xmldb_table, $newname)) {throw new ddl_exception('ddlunknownerror', null, 'table rename sql not generated');}$this->execute_sql_arr($sqlarr);}/*** This function will add the field to the table passed as arguments** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_field $xmldb_field Index object (full specs are required).* @return void*/public function add_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {// Check the field doesn't existif ($this->field_exists($xmldb_table, $xmldb_field)) {throw new ddl_exception('ddlfieldalreadyexists', $xmldb_field->getName());}// If NOT NULL and no default given (we ask the generator about the// *real* default that will be used) check the table is emptyif ($xmldb_field->getNotNull() && $this->generator->getDefaultValue($xmldb_field) === NULL && $this->mdb->count_records($xmldb_table->getName())) {throw new ddl_exception('ddlunknownerror', null, 'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .' cannot be added. Not null fields added to non empty tables require default value. Create skipped');}if (!$sqlarr = $this->generator->getAddFieldSQL($xmldb_table, $xmldb_field)) {throw new ddl_exception('ddlunknownerror', null, 'addfield sql not generated');}$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));}/*** This function will drop the field from the table passed as arguments** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_field $xmldb_field Index object (full specs are required).* @return void*/public function drop_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {if (!$this->table_exists($xmldb_table)) {throw new ddl_table_missing_exception($xmldb_table->getName());}// Check the field existsif (!$this->field_exists($xmldb_table, $xmldb_field)) {throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());}// Check for dependencies in the DB before performing any action$this->check_field_dependencies($xmldb_table, $xmldb_field);if (!$sqlarr = $this->generator->getDropFieldSQL($xmldb_table, $xmldb_field)) {throw new ddl_exception('ddlunknownerror', null, 'drop_field sql not generated');}$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));}/*** This function will change the type of the field in the table passed as arguments** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_field $xmldb_field Index object (full specs are required).* @return void*/public function change_field_type(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {if (!$this->table_exists($xmldb_table)) {throw new ddl_table_missing_exception($xmldb_table->getName());}// Check the field existsif (!$this->field_exists($xmldb_table, $xmldb_field)) {throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());}// Check for dependencies in the DB before performing any action$this->check_field_dependencies($xmldb_table, $xmldb_field);if (!$sqlarr = $this->generator->getAlterFieldSQL($xmldb_table, $xmldb_field)) {return; // probably nothing to do}$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));}/*** This function will change the precision of the field in the table passed as arguments** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_field $xmldb_field Index object (full specs are required).* @return void*/public function change_field_precision(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {// Just a wrapper over change_field_type. Does exactly the same processing$this->change_field_type($xmldb_table, $xmldb_field);}/*** This function will change the unsigned/signed of the field in the table passed as arguments** @deprecated since 2.3, only singed numbers are allowed now, migration is automatic* @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_field $xmldb_field Field object (full specs are required).* @return void*/public function change_field_unsigned(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {debugging('All unsigned numbers are converted to signed automatically during Moodle upgrade.');$this->change_field_type($xmldb_table, $xmldb_field);}/*** This function will change the nullability of the field in the table passed as arguments** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_field $xmldb_field Index object (full specs are required).* @return void*/public function change_field_notnull(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {// Just a wrapper over change_field_type. Does exactly the same processing$this->change_field_type($xmldb_table, $xmldb_field);}/*** This function will change the default of the field in the table passed as arguments* One null value in the default field means delete the default** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_field $xmldb_field Index object (full specs are required).* @return void*/public function change_field_default(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {if (!$this->table_exists($xmldb_table)) {throw new ddl_table_missing_exception($xmldb_table->getName());}// Check the field existsif (!$this->field_exists($xmldb_table, $xmldb_field)) {throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());}// Check for dependencies in the DB before performing any action$this->check_field_dependencies($xmldb_table, $xmldb_field);if (!$sqlarr = $this->generator->getModifyDefaultSQL($xmldb_table, $xmldb_field)) {return; //Empty array = nothing to do = no error}$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));}/*** This function will rename the field in the table passed as arguments* Before renaming the field, the function will check it exists** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_field $xmldb_field Index object (full specs are required).* @param string $newname New name of the field.* @return void*/public function rename_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field, $newname) {if (empty($newname)) {throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');}if (!$this->table_exists($xmldb_table)) {throw new ddl_table_missing_exception($xmldb_table->getName());}// Check the field existsif (!$this->field_exists($xmldb_table, $xmldb_field)) {throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());}// Check we have included full field specsif (!$xmldb_field->getType()) {throw new ddl_exception('ddlunknownerror', null,'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .' must contain full specs. Rename skipped');}// Check field isn't id. Renaming over that field is not allowedif ($xmldb_field->getName() == 'id') {throw new ddl_exception('ddlunknownerror', null,'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .' cannot be renamed. Rename skipped');}if (!$sqlarr = $this->generator->getRenameFieldSQL($xmldb_table, $xmldb_field, $newname)) {return; //Empty array = nothing to do = no error}$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));}/*** This function will check, for the given table and field, if there there is any dependency* preventing the field to be modified. It's used by all the public methods that perform any* DDL change on fields, throwing one ddl_dependency_exception if dependencies are found.** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_field $xmldb_field Index object (full specs are required).* @return void* @throws ddl_dependency_exception|ddl_field_missing_exception|ddl_table_missing_exception if dependency not met.*/private function check_field_dependencies(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {// Check the table existsif (!$this->table_exists($xmldb_table)) {throw new ddl_table_missing_exception($xmldb_table->getName());}// Check the field existsif (!$this->field_exists($xmldb_table, $xmldb_field)) {throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());}// Check the field isn't in use by any index in the tableif ($indexes = $this->mdb->get_indexes($xmldb_table->getName(), false)) {foreach ($indexes as $indexname => $index) {$columns = $index['columns'];if (in_array($xmldb_field->getName(), $columns)) {throw new ddl_dependency_exception('column', $xmldb_table->getName() . '->' . $xmldb_field->getName(),'index', $indexname . ' (' . implode(', ', $columns) . ')');}}}}/*** This function will create the key in the table passed as arguments** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_key $xmldb_key Index object (full specs are required).* @return void*/public function add_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {if ($xmldb_key->getType() == XMLDB_KEY_PRIMARY) { // Prevent PRIMARY to be added (only in create table, being serious :-P)throw new ddl_exception('ddlunknownerror', null, 'Primary Keys can be added at table create time only');}if (!$sqlarr = $this->generator->getAddKeySQL($xmldb_table, $xmldb_key)) {return; //Empty array = nothing to do = no error}$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));}/*** This function will drop the key in the table passed as arguments** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_key $xmldb_key Key object (full specs are required).* @return void*/public function drop_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {if ($xmldb_key->getType() == XMLDB_KEY_PRIMARY) { // Prevent PRIMARY to be dropped (only in drop table, being serious :-P)throw new ddl_exception('ddlunknownerror', null, 'Primary Keys can be deleted at table drop time only');}if (!$sqlarr = $this->generator->getDropKeySQL($xmldb_table, $xmldb_key)) {return; //Empty array = nothing to do = no error}$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));}/*** This function will rename the key in the table passed as arguments* Experimental. Shouldn't be used at all in normal installation/upgrade!** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_key $xmldb_key key object (full specs are required).* @param string $newname New name of the key.* @return void*/public function rename_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key, $newname) {debugging('rename_key() is one experimental feature. You must not use it in production!', DEBUG_DEVELOPER);// Check newname isn't emptyif (!$newname) {throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');}if (!$sqlarr = $this->generator->getRenameKeySQL($xmldb_table, $xmldb_key, $newname)) {throw new ddl_exception('ddlunknownerror', null, 'Some DBs do not support key renaming (MySQL, PostgreSQL, MsSQL). Rename skipped');}$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));}/*** This function will create the index in the table passed as arguments* Before creating the index, the function will check it doesn't exists** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_index $xmldb_intex Index object (full specs are required).* @return void*/public function add_index($xmldb_table, $xmldb_intex) {if (!$this->table_exists($xmldb_table)) {throw new ddl_table_missing_exception($xmldb_table->getName());}// Check index doesn't existif ($this->index_exists($xmldb_table, $xmldb_intex)) {throw new ddl_exception('ddlunknownerror', null,'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .' already exists. Create skipped');}if (!$sqlarr = $this->generator->getAddIndexSQL($xmldb_table, $xmldb_intex)) {throw new ddl_exception('ddlunknownerror', null, 'add_index sql not generated');}try {$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));} catch (ddl_change_structure_exception $e) {// There could be a problem with the index length related to the row format of the table.// If we are using utf8mb4 and the row format is 'compact' or 'redundant' then we need to change it over to// 'compressed' or 'dynamic'.if (method_exists($this->mdb, 'convert_table_row_format')) {$this->mdb->convert_table_row_format($xmldb_table->getName());$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));} else {// It's some other problem that we are currently not handling.throw $e;}}}/*** This function will drop the index in the table passed as arguments* Before dropping the index, the function will check it exists** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_index $xmldb_intex Index object (full specs are required).* @return void*/public function drop_index($xmldb_table, $xmldb_intex) {if (!$this->table_exists($xmldb_table)) {throw new ddl_table_missing_exception($xmldb_table->getName());}// Check index existsif (!$this->index_exists($xmldb_table, $xmldb_intex)) {throw new ddl_exception('ddlunknownerror', null,'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .' does not exist. Drop skipped');}if (!$sqlarr = $this->generator->getDropIndexSQL($xmldb_table, $xmldb_intex)) {throw new ddl_exception('ddlunknownerror', null, 'drop_index sql not generated');}$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));}/*** This function will rename the index in the table passed as arguments* Before renaming the index, the function will check it exists* Experimental. Shouldn't be used at all!** @param xmldb_table $xmldb_table Table object (just the name is mandatory).* @param xmldb_index $xmldb_intex Index object (full specs are required).* @param string $newname New name of the index.* @return void*/public function rename_index($xmldb_table, $xmldb_intex, $newname) {debugging('rename_index() is one experimental feature. You must not use it in production!', DEBUG_DEVELOPER);// Check newname isn't emptyif (!$newname) {throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');}// Check index existsif (!$this->index_exists($xmldb_table, $xmldb_intex)) {throw new ddl_exception('ddlunknownerror', null,'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .' does not exist. Rename skipped');}if (!$sqlarr = $this->generator->getRenameIndexSQL($xmldb_table, $xmldb_intex, $newname)) {throw new ddl_exception('ddlunknownerror', null, 'Some DBs do not support index renaming (MySQL). Rename skipped');}$this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));}/*** Get the list of install.xml files.** @return array*/public function get_install_xml_files(): array {global $CFG;require_once($CFG->libdir.'/adminlib.php');$files = [];$dbdirs = get_db_directories();foreach ($dbdirs as $dbdir) {$filename = "{$dbdir}/install.xml";if (file_exists($filename)) {$files[] = $filename;}}return $files;}/*** Reads the install.xml files for Moodle core and modules and returns an array of* xmldb_structure object with xmldb_table from these files.* @return xmldb_structure schema from install.xml files*/public function get_install_xml_schema() {global $CFG;require_once($CFG->libdir.'/adminlib.php');$schema = new xmldb_structure('export');$schema->setVersion($CFG->version);foreach ($this->get_install_xml_files() as $filename) {$xmldb_file = new xmldb_file($filename);if (!$xmldb_file->loadXMLStructure()) {continue;}$structure = $xmldb_file->getStructure();$tables = $structure->getTables();foreach ($tables as $table) {$table->setPrevious(null);$table->setNext(null);$schema->addTable($table);}}return $schema;}/*** Checks the database schema against a schema specified by an xmldb_structure object* @param xmldb_structure $schema export schema describing all known tables* @param array $options* @return array keyed by table name with array of difference messages as values*/public function check_database_schema(xmldb_structure $schema, array $options = null) {$alloptions = array('extratables' => true,'missingtables' => true,'extracolumns' => true,'missingcolumns' => true,'changedcolumns' => true,'missingindexes' => true,'extraindexes' => true);$typesmap = array('I' => XMLDB_TYPE_INTEGER,'R' => XMLDB_TYPE_INTEGER,'N' => XMLDB_TYPE_NUMBER,'F' => XMLDB_TYPE_NUMBER, // Nobody should be using floats!'C' => XMLDB_TYPE_CHAR,'X' => XMLDB_TYPE_TEXT,'B' => XMLDB_TYPE_BINARY,'T' => XMLDB_TYPE_TIMESTAMP,'D' => XMLDB_TYPE_DATETIME,);$options = (array)$options;$options = array_merge($alloptions, $options);// Note: the error descriptions are not supposed to be localised,// it is intended for developers and skilled admins only.$errors = array();/** @var string[] $dbtables */$dbtables = $this->mdb->get_tables(false);/** @var xmldb_table[] $tables */$tables = $schema->getTables();foreach ($tables as $table) {$tablename = $table->getName();if ($options['missingtables']) {// Missing tables are a fatal problem.if (empty($dbtables[$tablename])) {$errors[$tablename][] = "table is missing";continue;}}/** @var database_column_info[] $dbfields */$dbfields = $this->mdb->get_columns($tablename, false);$dbindexes = $this->mdb->get_indexes($tablename);/** @var xmldb_field[] $fields */$fields = $table->getFields();foreach ($fields as $field) {$fieldname = $field->getName();if (empty($dbfields[$fieldname])) {if ($options['missingcolumns']) {// Missing columns are a fatal problem.$errors[$tablename][] = "column '$fieldname' is missing";}} else if ($options['changedcolumns']) {$dbfield = $dbfields[$fieldname];if (!isset($typesmap[$dbfield->meta_type])) {$errors[$tablename][] = "column '$fieldname' has unsupported type '$dbfield->meta_type'";} else {$dbtype = $typesmap[$dbfield->meta_type];$type = $field->getType();if ($type == XMLDB_TYPE_FLOAT) {$type = XMLDB_TYPE_NUMBER;}if ($type != $dbtype) {if ($expected = array_search($type, $typesmap)) {$errors[$tablename][] = "column '$fieldname' has incorrect type '$dbfield->meta_type', expected '$expected'";} else {$errors[$tablename][] = "column '$fieldname' has incorrect type '$dbfield->meta_type'";}} else {if ($field->getNotNull() != $dbfield->not_null) {if ($field->getNotNull()) {$errors[$tablename][] = "column '$fieldname' should be NOT NULL ($dbfield->meta_type)";} else {$errors[$tablename][] = "column '$fieldname' should allow NULL ($dbfield->meta_type)";}}switch ($dbtype) {case XMLDB_TYPE_TEXT:case XMLDB_TYPE_BINARY:// No length check necessary - there is one size only now.break;case XMLDB_TYPE_NUMBER:$lengthmismatch = $field->getLength() != $dbfield->max_length;$decimalmismatch = $field->getDecimals() != $dbfield->scale;// Do not use floats in any new code, they are deprecated in XMLDB editor!if ($field->getType() != XMLDB_TYPE_FLOAT && ($lengthmismatch || $decimalmismatch)) {$size = "({$field->getLength()},{$field->getDecimals()})";$dbsize = "($dbfield->max_length,$dbfield->scale)";$errors[$tablename][] = "column '$fieldname' size is $dbsize,"." expected $size ($dbfield->meta_type)";}break;case XMLDB_TYPE_CHAR:// This is not critical, but they should ideally match.if ($field->getLength() != $dbfield->max_length) {$errors[$tablename][] = "column '$fieldname' length is $dbfield->max_length,"." expected {$field->getLength()} ($dbfield->meta_type)";}break;case XMLDB_TYPE_INTEGER:// Integers may be bigger in some DBs.$length = $field->getLength();if ($length > 18) {// Integers are not supposed to be bigger than 18.$length = 18;}if ($length > $dbfield->max_length) {$errors[$tablename][] = "column '$fieldname' length is $dbfield->max_length,"." expected at least {$field->getLength()} ($dbfield->meta_type)";}break;case XMLDB_TYPE_TIMESTAMP:$errors[$tablename][] = "column '$fieldname' is a timestamp,"." this type is not supported ($dbfield->meta_type)";continue 2;case XMLDB_TYPE_DATETIME:$errors[$tablename][] = "column '$fieldname' is a datetime,"."this type is not supported ($dbfield->meta_type)";continue 2;default:// Report all other unsupported types as problems.$errors[$tablename][] = "column '$fieldname' has unknown type ($dbfield->meta_type)";continue 2;}// Note: The empty string defaults are a bit messy...if ($field->getDefault() != $dbfield->default_value) {$default = is_null($field->getDefault()) ? 'NULL' : $field->getDefault();$dbdefault = is_null($dbfield->default_value) ? 'NULL' : $dbfield->default_value;$errors[$tablename][] = "column '$fieldname' has default '$dbdefault', expected '$default' ($dbfield->meta_type)";}}}}unset($dbfields[$fieldname]);}// Check for missing indexes/keys.if ($options['missingindexes']) {// Check the foreign keys.if ($keys = $table->getKeys()) {foreach ($keys as $key) {// Primary keys are skipped.if ($key->getType() == XMLDB_KEY_PRIMARY) {continue;}$keyname = $key->getName();// Create the interim index.$index = new xmldb_index('anyname');$index->setFields($key->getFields());switch ($key->getType()) {case XMLDB_KEY_UNIQUE:case XMLDB_KEY_FOREIGN_UNIQUE:$index->setUnique(true);break;case XMLDB_KEY_FOREIGN:$index->setUnique(false);break;}if (!$this->index_exists($table, $index)) {$errors[$tablename][] = $this->get_missing_index_error($table, $index, $keyname);} else {$this->remove_index_from_dbindex($dbindexes, $index);}}}// Check the indexes.if ($indexes = $table->getIndexes()) {foreach ($indexes as $index) {if (!$this->index_exists($table, $index)) {$errors[$tablename][] = $this->get_missing_index_error($table, $index, $index->getName());} else {$this->remove_index_from_dbindex($dbindexes, $index);}}}}// Check if we should show the extra indexes.if ($options['extraindexes']) {// Hack - skip for table 'search_simpledb_index' as this plugin adds indexes dynamically on install// which are not included in install.xml. See search/engine/simpledb/db/install.php.if ($tablename != 'search_simpledb_index') {foreach ($dbindexes as $indexname => $index) {$errors[$tablename][] = "Unexpected index '$indexname'.";}}}// Check for extra columns (indicates unsupported hacks) - modify install.xml if you want to pass validation.foreach ($dbfields as $fieldname => $dbfield) {if ($options['extracolumns']) {$errors[$tablename][] = "column '$fieldname' is not expected ($dbfield->meta_type)";}}unset($dbtables[$tablename]);}if ($options['extratables']) {// Look for unsupported tables - local custom tables should be in /local/xxxx/db/install.xml file.// If there is no prefix, we can not say if table is ours, sorry.if ($this->generator->prefix !== '') {foreach ($dbtables as $tablename => $unused) {if (strpos($tablename, 'pma_') === 0) {// Ignore phpmyadmin tables.continue;}if (strpos($tablename, 'test') === 0) {// Legacy simple test db tables need to be eventually removed,// report them as problems!$errors[$tablename][] = "table is not expected (it may be a leftover after Simpletest unit tests)";} else {$errors[$tablename][] = "table is not expected";}}}}return $errors;}/*** Returns a string describing the missing index error.** @param xmldb_table $table* @param xmldb_index $index* @param string $indexname* @return string*/private function get_missing_index_error(xmldb_table $table, xmldb_index $index, string $indexname): string {$sqlarr = $this->generator->getAddIndexSQL($table, $index);$sqlarr = $this->generator->getEndedStatements($sqlarr);$sqltoadd = reset($sqlarr);return "Missing index '" . $indexname . "' " . "(" . $index->readableInfo() . "). \n" . $sqltoadd;}/*** Removes an index from the array $dbindexes if it is found.** @param array $dbindexes* @param xmldb_index $index*/private function remove_index_from_dbindex(array &$dbindexes, xmldb_index $index) {foreach ($dbindexes as $key => $dbindex) {if ($dbindex['columns'] == $index->getFields()) {unset($dbindexes[$key]);}}}}