Differences Between: [Versions 310 and 311] [Versions 39 and 311]
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 switch ($dbtype) { 1043 case XMLDB_TYPE_TEXT: 1044 case XMLDB_TYPE_BINARY: 1045 // No length check necessary - there is one size only now. 1046 break; 1047 1048 case XMLDB_TYPE_NUMBER: 1049 $lengthmismatch = $field->getLength() != $dbfield->max_length; 1050 $decimalmismatch = $field->getDecimals() != $dbfield->scale; 1051 // Do not use floats in any new code, they are deprecated in XMLDB editor! 1052 if ($field->getType() != XMLDB_TYPE_FLOAT && ($lengthmismatch || $decimalmismatch)) { 1053 $size = "({$field->getLength()},{$field->getDecimals()})"; 1054 $dbsize = "($dbfield->max_length,$dbfield->scale)"; 1055 $errors[$tablename][] = "column '$fieldname' size is $dbsize,". 1056 " expected $size ($dbfield->meta_type)"; 1057 } 1058 break; 1059 1060 case XMLDB_TYPE_CHAR: 1061 // This is not critical, but they should ideally match. 1062 if ($field->getLength() != $dbfield->max_length) { 1063 $errors[$tablename][] = "column '$fieldname' length is $dbfield->max_length,". 1064 " expected {$field->getLength()} ($dbfield->meta_type)"; 1065 } 1066 break; 1067 1068 case XMLDB_TYPE_INTEGER: 1069 // Integers may be bigger in some DBs. 1070 $length = $field->getLength(); 1071 if ($length > 18) { 1072 // Integers are not supposed to be bigger than 18. 1073 $length = 18; 1074 } 1075 if ($length > $dbfield->max_length) { 1076 $errors[$tablename][] = "column '$fieldname' length is $dbfield->max_length,". 1077 " expected at least {$field->getLength()} ($dbfield->meta_type)"; 1078 } 1079 break; 1080 1081 case XMLDB_TYPE_TIMESTAMP: 1082 $errors[$tablename][] = "column '$fieldname' is a timestamp,". 1083 " this type is not supported ($dbfield->meta_type)"; 1084 continue 2; 1085 1086 case XMLDB_TYPE_DATETIME: 1087 $errors[$tablename][] = "column '$fieldname' is a datetime,". 1088 "this type is not supported ($dbfield->meta_type)"; 1089 continue 2; 1090 1091 default: 1092 // Report all other unsupported types as problems. 1093 $errors[$tablename][] = "column '$fieldname' has unknown type ($dbfield->meta_type)"; 1094 continue 2; 1095 1096 } 1097 1098 // Note: The empty string defaults are a bit messy... 1099 if ($field->getDefault() != $dbfield->default_value) { 1100 $default = is_null($field->getDefault()) ? 'NULL' : $field->getDefault(); 1101 $dbdefault = is_null($dbfield->default_value) ? 'NULL' : $dbfield->default_value; 1102 $errors[$tablename][] = "column '$fieldname' has default '$dbdefault', expected '$default' ($dbfield->meta_type)"; 1103 } 1104 } 1105 } 1106 } 1107 unset($dbfields[$fieldname]); 1108 } 1109 1110 // Check for missing indexes/keys. 1111 if ($options['missingindexes']) { 1112 // Check the foreign keys. 1113 if ($keys = $table->getKeys()) { 1114 foreach ($keys as $key) { 1115 // Primary keys are skipped. 1116 if ($key->getType() == XMLDB_KEY_PRIMARY) { 1117 continue; 1118 } 1119 1120 $keyname = $key->getName(); 1121 1122 // Create the interim index. 1123 $index = new xmldb_index('anyname'); 1124 $index->setFields($key->getFields()); 1125 switch ($key->getType()) { 1126 case XMLDB_KEY_UNIQUE: 1127 case XMLDB_KEY_FOREIGN_UNIQUE: 1128 $index->setUnique(true); 1129 break; 1130 case XMLDB_KEY_FOREIGN: 1131 $index->setUnique(false); 1132 break; 1133 } 1134 if (!$this->index_exists($table, $index)) { 1135 $errors[$tablename][] = $this->get_missing_index_error($table, $index, $keyname); 1136 } else { 1137 $this->remove_index_from_dbindex($dbindexes, $index); 1138 } 1139 } 1140 } 1141 1142 // Check the indexes. 1143 if ($indexes = $table->getIndexes()) { 1144 foreach ($indexes as $index) { 1145 if (!$this->index_exists($table, $index)) { 1146 $errors[$tablename][] = $this->get_missing_index_error($table, $index, $index->getName()); 1147 } else { 1148 $this->remove_index_from_dbindex($dbindexes, $index); 1149 } 1150 } 1151 } 1152 } 1153 1154 // Check if we should show the extra indexes. 1155 if ($options['extraindexes']) { 1156 // Hack - skip for table 'search_simpledb_index' as this plugin adds indexes dynamically on install 1157 // which are not included in install.xml. See search/engine/simpledb/db/install.php. 1158 if ($tablename != 'search_simpledb_index') { 1159 foreach ($dbindexes as $indexname => $index) { 1160 $errors[$tablename][] = "Unexpected index '$indexname'."; 1161 } 1162 } 1163 } 1164 1165 // Check for extra columns (indicates unsupported hacks) - modify install.xml if you want to pass validation. 1166 foreach ($dbfields as $fieldname => $dbfield) { 1167 if ($options['extracolumns']) { 1168 $errors[$tablename][] = "column '$fieldname' is not expected ($dbfield->meta_type)"; 1169 } 1170 } 1171 unset($dbtables[$tablename]); 1172 } 1173 1174 if ($options['extratables']) { 1175 // Look for unsupported tables - local custom tables should be in /local/xxxx/db/install.xml file. 1176 // If there is no prefix, we can not say if table is ours, sorry. 1177 if ($this->generator->prefix !== '') { 1178 foreach ($dbtables as $tablename => $unused) { 1179 if (strpos($tablename, 'pma_') === 0) { 1180 // Ignore phpmyadmin tables. 1181 continue; 1182 } 1183 if (strpos($tablename, 'test') === 0) { 1184 // Legacy simple test db tables need to be eventually removed, 1185 // report them as problems! 1186 $errors[$tablename][] = "table is not expected (it may be a leftover after Simpletest unit tests)"; 1187 } else { 1188 $errors[$tablename][] = "table is not expected"; 1189 } 1190 } 1191 } 1192 } 1193 1194 return $errors; 1195 } 1196 1197 /** 1198 * Returns a string describing the missing index error. 1199 * 1200 * @param xmldb_table $table 1201 * @param xmldb_index $index 1202 * @param string $indexname 1203 * @return string 1204 */ 1205 private function get_missing_index_error(xmldb_table $table, xmldb_index $index, string $indexname): string { 1206 $sqlarr = $this->generator->getAddIndexSQL($table, $index); 1207 $sqlarr = $this->generator->getEndedStatements($sqlarr); 1208 $sqltoadd = reset($sqlarr); 1209 1210 return "Missing index '" . $indexname . "' " . "(" . $index->readableInfo() . "). \n" . $sqltoadd; 1211 } 1212 1213 /** 1214 * Removes an index from the array $dbindexes if it is found. 1215 * 1216 * @param array $dbindexes 1217 * @param xmldb_index $index 1218 */ 1219 private function remove_index_from_dbindex(array &$dbindexes, xmldb_index $index) { 1220 foreach ($dbindexes as $key => $dbindex) { 1221 if ($dbindex['columns'] == $index->getFields()) { 1222 unset($dbindexes[$key]); 1223 } 1224 } 1225 } 1226 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body