MDL-69542 enrol_lti: add entity repositories
[moodle.git] / lib / ddl / database_manager.php
blob7fdd3be58b87109bcfc31c18e75683048f93d46e
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.
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/>.
17 /**
18 * Database manager instance is responsible for all database structure modifications.
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
27 defined('MOODLE_INTERNAL') || die();
29 /**
30 * Database manager instance is responsible for all database structure modifications.
32 * It is using db specific generators to find out the correct SQL syntax to do that.
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
40 class database_manager {
42 /** @var moodle_database A moodle_database driver specific instance.*/
43 protected $mdb;
45 /** @var sql_generator A driver specific SQL generator instance. Public because XMLDB editor needs to access it.*/
46 public $generator;
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.
53 public function __construct($mdb, $generator) {
54 $this->mdb = $mdb;
55 $this->generator = $generator;
58 /**
59 * Releases all resources
61 public function dispose() {
62 if ($this->generator) {
63 $this->generator->dispose();
64 $this->generator = null;
66 $this->mdb = null;
69 /**
70 * This function will execute an array of SQL commands.
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.
76 protected function execute_sql_arr(array $sqlarr, $tablenames = null) {
77 $this->mdb->change_database_structure($sqlarr, $tablenames);
80 /**
81 * Execute a given sql command string.
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.
86 protected function execute_sql($sql) {
87 $this->mdb->change_database_structure($sql);
90 /**
91 * Given one xmldb_table, check if it exists in DB (true/false).
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.
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!');
100 return $this->generator->table_exists($table);
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.
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;
119 // Do not test if table exists because it is slow
121 if (!$sqlarr = $this->generator->getResetSequenceSQL($table)) {
122 throw new ddl_exception('ddlunknownerror', null, 'table reset sequence sql not generated');
125 $this->execute_sql_arr($sqlarr, array($tablename));
129 * Given one xmldb_field, check if it exists in DB (true/false).
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
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();
144 // Check the table exists
145 if (!$this->table_exists($table)) {
146 throw new ddl_table_missing_exception($tablename);
149 if (is_string($field)) {
150 $fieldname = $field;
151 } else {
152 // Calculate the name of the table
153 $fieldname = $field->getName();
156 // Get list of fields in table
157 $columns = $this->mdb->get_columns($tablename);
159 $exists = array_key_exists($fieldname, $columns);
161 return $exists;
165 * Given one xmldb_index, the function returns the name of the index in DB
166 * of false if it doesn't exist
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.
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();
178 // Check the table exists
179 if (!$this->table_exists($xmldb_table)) {
180 throw new ddl_table_missing_exception($tablename);
183 // Extract index columns
184 $indcolumns = $xmldb_index->getFields();
186 // Get list of indexes in table
187 $indexes = $this->mdb->get_indexes($tablename);
189 $return = array();
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;
206 if ($return and $returnall) {
207 return $return;
210 // Arriving here, index not found
211 return false;
215 * Given one xmldb_index, check if it exists in DB (true/false).
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.
221 public function index_exists(xmldb_table $xmldb_table, xmldb_index $xmldb_index) {
222 if (!$this->table_exists($xmldb_table)) {
223 return false;
225 return ($this->find_index_name($xmldb_table, $xmldb_index) !== false);
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
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
239 public function find_key_name(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {
241 $keycolumns = $xmldb_key->getFields();
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
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;
272 // And simply, return the official name
273 return $this->generator->getNameForObject($xmldb_table->getName(), implode(', ', $xmldb_key->getFields()), $suffix);
278 * This function will delete all tables found in XMLDB file from db
280 * @param string $file Full path to the XML file to be used.
281 * @return void
283 public function delete_tables_from_xmldb_file($file) {
285 $xmldb_file = new xmldb_file($file);
287 if (!$xmldb_file->fileExists()) {
288 throw new ddl_exception('ddlxmlfileerror', null, 'File does not exist');
291 $loaded = $xmldb_file->loadXMLStructure();
292 $structure = $xmldb_file->getStructure();
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));
301 throw new ddl_exception('ddlxmlfileerror', null, 'not loaded??');
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);
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.
320 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
321 * @return void
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());
329 if (!$sqlarr = $this->generator->getDropTableSQL($xmldb_table)) {
330 throw new ddl_exception('ddlunknownerror', null, 'table drop sql not generated');
332 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
334 $this->generator->cleanup_after_drop($xmldb_table);
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.
342 private function load_xmldb_file($file) {
343 $xmldb_file = new xmldb_file($file);
345 if (!$xmldb_file->fileExists()) {
346 throw new ddl_exception('ddlxmlfileerror', null, 'File does not exist');
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));
357 throw new ddl_exception('ddlxmlfileerror', null, 'not loaded??');
360 return $xmldb_file;
364 * This function will load one entire XMLDB file and call install_from_xmldb_structure.
366 * @param string $file full path to the XML file to be used
367 * @return void
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);
376 * This function will load one entire XMLDB file and call install_from_xmldb_structure.
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)
383 public function install_one_table_from_xmldb_file($file, $tablename, $cachestructures = false) {
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;
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);
400 $targettable->setNext(NULL);
401 $targettable->setPrevious(NULL);
403 $tempstructure = new xmldb_structure('temp');
404 $tempstructure->addTable($targettable);
405 $this->install_from_xmldb_structure($tempstructure);
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.
412 * @param stdClass $xmldb_structure xmldb_structure object.
413 * @return void
415 public function install_from_xmldb_structure($xmldb_structure) {
417 if (!$sqlarr = $this->generator->getCreateStructureSQL($xmldb_structure)) {
418 return; // nothing to do
421 $tablenames = array();
422 foreach ($xmldb_structure as $xmldb_table) {
423 if ($xmldb_table instanceof xmldb_table) {
424 $tablenames[] = $xmldb_table->getName();
427 $this->execute_sql_arr($sqlarr, $tablenames);
431 * This function will create the table passed as argument with all its
432 * fields/keys/indexes/sequences, everything based in the XMLDB object
434 * @param xmldb_table $xmldb_table Table object (full specs are required).
435 * @return void
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());
443 if (!$sqlarr = $this->generator->getCreateTableSQL($xmldb_table)) {
444 throw new ddl_exception('ddlunknownerror', null, 'table create sql not generated');
446 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
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
453 * If table already exists ddl_exception will be thrown, please make sure
454 * the table name does not collide with existing normal table!
456 * @param xmldb_table $xmldb_table Table object (full specs are required).
457 * @return void
459 public function create_temp_table(xmldb_table $xmldb_table) {
461 // Check table doesn't exist
462 if ($this->table_exists($xmldb_table)) {
463 throw new ddl_exception('ddltablealreadyexists', $xmldb_table->getName());
466 if (!$sqlarr = $this->generator->getCreateTempTableSQL($xmldb_table)) {
467 throw new ddl_exception('ddlunknownerror', null, 'temp table create sql not generated');
469 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
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
476 * It is recommended to drop temp table when not used anymore.
478 * @deprecated since 2.3, use drop_table() for all table types
479 * @param xmldb_table $xmldb_table Table object.
480 * @return void
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);
488 * This function will rename the table passed as argument
489 * Before renaming the index, the function will check it exists
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
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');
501 $check = new xmldb_table($newname);
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());
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');
517 if (!$sqlarr = $this->generator->getRenameTableSQL($xmldb_table, $newname)) {
518 throw new ddl_exception('ddlunknownerror', null, 'table rename sql not generated');
521 $this->execute_sql_arr($sqlarr);
525 * This function will add the field to the table passed as arguments
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
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());
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');
544 if (!$sqlarr = $this->generator->getAddFieldSQL($xmldb_table, $xmldb_field)) {
545 throw new ddl_exception('ddlunknownerror', null, 'addfield sql not generated');
547 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
551 * This function will drop the field from the table passed as arguments
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
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());
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());
565 // Check for dependencies in the DB before performing any action
566 $this->check_field_dependencies($xmldb_table, $xmldb_field);
568 if (!$sqlarr = $this->generator->getDropFieldSQL($xmldb_table, $xmldb_field)) {
569 throw new ddl_exception('ddlunknownerror', null, 'drop_field sql not generated');
572 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
576 * This function will change the type of the field in the table passed as arguments
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
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());
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());
590 // Check for dependencies in the DB before performing any action
591 $this->check_field_dependencies($xmldb_table, $xmldb_field);
593 if (!$sqlarr = $this->generator->getAlterFieldSQL($xmldb_table, $xmldb_field)) {
594 return; // probably nothing to do
597 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
601 * This function will change the precision of the field in the table passed as arguments
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
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);
613 * This function will change the unsigned/signed of the field in the table passed as arguments
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
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);
626 * This function will change the nullability of the field in the table passed as arguments
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
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);
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
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
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());
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());
653 // Check for dependencies in the DB before performing any action
654 $this->check_field_dependencies($xmldb_table, $xmldb_field);
656 if (!$sqlarr = $this->generator->getModifyDefaultSQL($xmldb_table, $xmldb_field)) {
657 return; //Empty array = nothing to do = no error
660 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
664 * This function will rename the field in the table passed as arguments
665 * Before renaming the field, the function will check it exists
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
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');
677 if (!$this->table_exists($xmldb_table)) {
678 throw new ddl_table_missing_exception($xmldb_table->getName());
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());
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');
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');
700 if (!$sqlarr = $this->generator->getRenameFieldSQL($xmldb_table, $xmldb_field, $newname)) {
701 return; //Empty array = nothing to do = no error
704 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
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.
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.
717 private function check_field_dependencies(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
719 // Check the table exists
720 if (!$this->table_exists($xmldb_table)) {
721 throw new ddl_table_missing_exception($xmldb_table->getName());
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());
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) . ')');
742 * This function will create the key in the table passed as arguments
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
748 public function add_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {
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');
754 if (!$sqlarr = $this->generator->getAddKeySQL($xmldb_table, $xmldb_key)) {
755 return; //Empty array = nothing to do = no error
758 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
762 * This function will drop the key in the table passed as arguments
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
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');
773 if (!$sqlarr = $this->generator->getDropKeySQL($xmldb_table, $xmldb_key)) {
774 return; //Empty array = nothing to do = no error
777 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
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!
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
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);
792 // Check newname isn't empty
793 if (!$newname) {
794 throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
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');
801 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
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
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
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());
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');
824 if (!$sqlarr = $this->generator->getAddIndexSQL($xmldb_table, $xmldb_intex)) {
825 throw new ddl_exception('ddlunknownerror', null, 'add_index sql not generated');
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;
845 * This function will drop the index in the table passed as arguments
846 * Before dropping the index, the function will check it exists
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
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());
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');
864 if (!$sqlarr = $this->generator->getDropIndexSQL($xmldb_table, $xmldb_intex)) {
865 throw new ddl_exception('ddlunknownerror', null, 'drop_index sql not generated');
868 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
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!
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
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);
884 // Check newname isn't empty
885 if (!$newname) {
886 throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
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');
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');
900 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
904 * Get the list of install.xml files.
906 * @return array
908 public function get_install_xml_files(): array {
909 global $CFG;
910 require_once($CFG->libdir.'/adminlib.php');
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;
921 return $files;
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
929 public function get_install_xml_schema() {
930 global $CFG;
931 require_once($CFG->libdir.'/adminlib.php');
933 $schema = new xmldb_structure('export');
934 $schema->setVersion($CFG->version);
936 foreach ($this->get_install_xml_files() as $filename) {
937 $xmldb_file = new xmldb_file($filename);
938 if (!$xmldb_file->loadXMLStructure()) {
939 continue;
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);
949 return $schema;
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
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
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,
981 $options = (array)$options;
982 $options = array_merge($alloptions, $options);
984 // Note: the error descriptions are not supposed to be localised,
985 // it is intended for developers and skilled admins only.
986 $errors = array();
988 /** @var string[] $dbtables */
989 $dbtables = $this->mdb->get_tables(false);
990 /** @var xmldb_table[] $tables */
991 $tables = $schema->getTables();
993 foreach ($tables as $table) {
994 $tablename = $table->getName();
996 if ($options['missingtables']) {
997 // Missing tables are a fatal problem.
998 if (empty($dbtables[$tablename])) {
999 $errors[$tablename][] = "table is missing";
1000 continue;
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();
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";
1017 } else if ($options['changedcolumns']) {
1018 $dbfield = $dbfields[$fieldname];
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;
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'";
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)";
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;
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)";
1058 break;
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)";
1066 break;
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;
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)";
1079 break;
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;
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;
1091 default:
1092 // Report all other unsupported types as problems.
1093 $errors[$tablename][] = "column '$fieldname' has unknown type ($dbfield->meta_type)";
1094 continue 2;
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)";
1107 unset($dbfields[$fieldname]);
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;
1120 $keyname = $key->getName();
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;
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);
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);
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'.";
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)";
1171 unset($dbtables[$tablename]);
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;
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";
1194 return $errors;
1198 * Returns a string describing the missing index error.
1200 * @param xmldb_table $table
1201 * @param xmldb_index $index
1202 * @param string $indexname
1203 * @return string
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);
1210 return "Missing index '" . $indexname . "' " . "(" . $index->readableInfo() . "). \n" . $sqltoadd;
1214 * Removes an index from the array $dbindexes if it is found.
1216 * @param array $dbindexes
1217 * @param xmldb_index $index
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]);