Merge branch 'MDL-67827-37' of git://github.com/andrewnicols/moodle into MOODLE_37_STABLE
[moodle.git] / lib / ddl / database_manager.php
blob7ddf633691dda325e69b7cfdaf6935a83d0c815a
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()));
336 * Load an install.xml file, checking that it exists, and that the structure is OK.
337 * @param string $file the full path to the XMLDB file.
338 * @return xmldb_file the loaded file.
340 private function load_xmldb_file($file) {
341 $xmldb_file = new xmldb_file($file);
343 if (!$xmldb_file->fileExists()) {
344 throw new ddl_exception('ddlxmlfileerror', null, 'File does not exist');
347 $loaded = $xmldb_file->loadXMLStructure();
348 if (!$loaded || !$xmldb_file->isLoaded()) {
349 // Show info about the error if we can find it
350 if ($structure = $xmldb_file->getStructure()) {
351 if ($errors = $structure->getAllErrors()) {
352 throw new ddl_exception('ddlxmlfileerror', null, 'Errors found in XMLDB file: '. implode (', ', $errors));
355 throw new ddl_exception('ddlxmlfileerror', null, 'not loaded??');
358 return $xmldb_file;
362 * This function will load one entire XMLDB file and call install_from_xmldb_structure.
364 * @param string $file full path to the XML file to be used
365 * @return void
367 public function install_from_xmldb_file($file) {
368 $xmldb_file = $this->load_xmldb_file($file);
369 $xmldb_structure = $xmldb_file->getStructure();
370 $this->install_from_xmldb_structure($xmldb_structure);
374 * This function will load one entire XMLDB file and call install_from_xmldb_structure.
376 * @param string $file full path to the XML file to be used
377 * @param string $tablename the name of the table.
378 * @param bool $cachestructures boolean to decide if loaded xmldb structures can be safely cached
379 * useful for testunits loading the enormous main xml file hundred of times (100x)
381 public function install_one_table_from_xmldb_file($file, $tablename, $cachestructures = false) {
383 static $xmldbstructurecache = array(); // To store cached structures
384 if (!empty($xmldbstructurecache) && array_key_exists($file, $xmldbstructurecache)) {
385 $xmldb_structure = $xmldbstructurecache[$file];
386 } else {
387 $xmldb_file = $this->load_xmldb_file($file);
388 $xmldb_structure = $xmldb_file->getStructure();
389 if ($cachestructures) {
390 $xmldbstructurecache[$file] = $xmldb_structure;
394 $targettable = $xmldb_structure->getTable($tablename);
395 if (is_null($targettable)) {
396 throw new ddl_exception('ddlunknowntable', null, 'The table ' . $tablename . ' is not defined in file ' . $file);
398 $targettable->setNext(NULL);
399 $targettable->setPrevious(NULL);
401 $tempstructure = new xmldb_structure('temp');
402 $tempstructure->addTable($targettable);
403 $this->install_from_xmldb_structure($tempstructure);
407 * This function will generate all the needed SQL statements, specific for each
408 * RDBMS type and, finally, it will execute all those statements against the DB.
410 * @param stdClass $xmldb_structure xmldb_structure object.
411 * @return void
413 public function install_from_xmldb_structure($xmldb_structure) {
415 if (!$sqlarr = $this->generator->getCreateStructureSQL($xmldb_structure)) {
416 return; // nothing to do
419 $tablenames = array();
420 foreach ($xmldb_structure as $xmldb_table) {
421 if ($xmldb_table instanceof xmldb_table) {
422 $tablenames[] = $xmldb_table->getName();
425 $this->execute_sql_arr($sqlarr, $tablenames);
429 * This function will create the table passed as argument with all its
430 * fields/keys/indexes/sequences, everything based in the XMLDB object
432 * @param xmldb_table $xmldb_table Table object (full specs are required).
433 * @return void
435 public function create_table(xmldb_table $xmldb_table) {
436 // Check table doesn't exist
437 if ($this->table_exists($xmldb_table)) {
438 throw new ddl_exception('ddltablealreadyexists', $xmldb_table->getName());
441 if (!$sqlarr = $this->generator->getCreateTableSQL($xmldb_table)) {
442 throw new ddl_exception('ddlunknownerror', null, 'table create sql not generated');
444 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
448 * This function will create the temporary table passed as argument with all its
449 * fields/keys/indexes/sequences, everything based in the XMLDB object
451 * If table already exists ddl_exception will be thrown, please make sure
452 * the table name does not collide with existing normal table!
454 * @param xmldb_table $xmldb_table Table object (full specs are required).
455 * @return void
457 public function create_temp_table(xmldb_table $xmldb_table) {
459 // Check table doesn't exist
460 if ($this->table_exists($xmldb_table)) {
461 throw new ddl_exception('ddltablealreadyexists', $xmldb_table->getName());
464 if (!$sqlarr = $this->generator->getCreateTempTableSQL($xmldb_table)) {
465 throw new ddl_exception('ddlunknownerror', null, 'temp table create sql not generated');
467 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
471 * This function will drop the temporary table passed as argument with all its
472 * fields/keys/indexes/sequences, everything based in the XMLDB object
474 * It is recommended to drop temp table when not used anymore.
476 * @deprecated since 2.3, use drop_table() for all table types
477 * @param xmldb_table $xmldb_table Table object.
478 * @return void
480 public function drop_temp_table(xmldb_table $xmldb_table) {
481 debugging('database_manager::drop_temp_table() is deprecated, use database_manager::drop_table() instead');
482 $this->drop_table($xmldb_table);
486 * This function will rename the table passed as argument
487 * Before renaming the index, the function will check it exists
489 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
490 * @param string $newname New name of the index.
491 * @return void
493 public function rename_table(xmldb_table $xmldb_table, $newname) {
494 // Check newname isn't empty
495 if (!$newname) {
496 throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
499 $check = new xmldb_table($newname);
501 // Check table already renamed
502 if (!$this->table_exists($xmldb_table)) {
503 if ($this->table_exists($check)) {
504 throw new ddl_exception('ddlunknownerror', null, 'table probably already renamed');
505 } else {
506 throw new ddl_table_missing_exception($xmldb_table->getName());
510 // Check new table doesn't exist
511 if ($this->table_exists($check)) {
512 throw new ddl_exception('ddltablealreadyexists', $check->getName(), 'can not rename table');
515 if (!$sqlarr = $this->generator->getRenameTableSQL($xmldb_table, $newname)) {
516 throw new ddl_exception('ddlunknownerror', null, 'table rename sql not generated');
519 $this->execute_sql_arr($sqlarr);
523 * This function will add the field to the table passed as arguments
525 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
526 * @param xmldb_field $xmldb_field Index object (full specs are required).
527 * @return void
529 public function add_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
530 // Check the field doesn't exist
531 if ($this->field_exists($xmldb_table, $xmldb_field)) {
532 throw new ddl_exception('ddlfieldalreadyexists', $xmldb_field->getName());
535 // If NOT NULL and no default given (we ask the generator about the
536 // *real* default that will be used) check the table is empty
537 if ($xmldb_field->getNotNull() && $this->generator->getDefaultValue($xmldb_field) === NULL && $this->mdb->count_records($xmldb_table->getName())) {
538 throw new ddl_exception('ddlunknownerror', null, 'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .
539 ' cannot be added. Not null fields added to non empty tables require default value. Create skipped');
542 if (!$sqlarr = $this->generator->getAddFieldSQL($xmldb_table, $xmldb_field)) {
543 throw new ddl_exception('ddlunknownerror', null, 'addfield sql not generated');
545 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
549 * This function will drop the field from the table passed as arguments
551 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
552 * @param xmldb_field $xmldb_field Index object (full specs are required).
553 * @return void
555 public function drop_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
556 if (!$this->table_exists($xmldb_table)) {
557 throw new ddl_table_missing_exception($xmldb_table->getName());
559 // Check the field exists
560 if (!$this->field_exists($xmldb_table, $xmldb_field)) {
561 throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
563 // Check for dependencies in the DB before performing any action
564 $this->check_field_dependencies($xmldb_table, $xmldb_field);
566 if (!$sqlarr = $this->generator->getDropFieldSQL($xmldb_table, $xmldb_field)) {
567 throw new ddl_exception('ddlunknownerror', null, 'drop_field sql not generated');
570 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
574 * This function will change the type of the field in the table passed as arguments
576 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
577 * @param xmldb_field $xmldb_field Index object (full specs are required).
578 * @return void
580 public function change_field_type(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
581 if (!$this->table_exists($xmldb_table)) {
582 throw new ddl_table_missing_exception($xmldb_table->getName());
584 // Check the field exists
585 if (!$this->field_exists($xmldb_table, $xmldb_field)) {
586 throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
588 // Check for dependencies in the DB before performing any action
589 $this->check_field_dependencies($xmldb_table, $xmldb_field);
591 if (!$sqlarr = $this->generator->getAlterFieldSQL($xmldb_table, $xmldb_field)) {
592 return; // probably nothing to do
595 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
599 * This function will change the precision of the field in the table passed as arguments
601 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
602 * @param xmldb_field $xmldb_field Index object (full specs are required).
603 * @return void
605 public function change_field_precision(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
606 // Just a wrapper over change_field_type. Does exactly the same processing
607 $this->change_field_type($xmldb_table, $xmldb_field);
611 * This function will change the unsigned/signed of the field in the table passed as arguments
613 * @deprecated since 2.3, only singed numbers are allowed now, migration is automatic
614 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
615 * @param xmldb_field $xmldb_field Field object (full specs are required).
616 * @return void
618 public function change_field_unsigned(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
619 debugging('All unsigned numbers are converted to signed automatically during Moodle upgrade.');
620 $this->change_field_type($xmldb_table, $xmldb_field);
624 * This function will change the nullability of the field in the table passed as arguments
626 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
627 * @param xmldb_field $xmldb_field Index object (full specs are required).
628 * @return void
630 public function change_field_notnull(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
631 // Just a wrapper over change_field_type. Does exactly the same processing
632 $this->change_field_type($xmldb_table, $xmldb_field);
636 * This function will change the default of the field in the table passed as arguments
637 * One null value in the default field means delete the default
639 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
640 * @param xmldb_field $xmldb_field Index object (full specs are required).
641 * @return void
643 public function change_field_default(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
644 if (!$this->table_exists($xmldb_table)) {
645 throw new ddl_table_missing_exception($xmldb_table->getName());
647 // Check the field exists
648 if (!$this->field_exists($xmldb_table, $xmldb_field)) {
649 throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
651 // Check for dependencies in the DB before performing any action
652 $this->check_field_dependencies($xmldb_table, $xmldb_field);
654 if (!$sqlarr = $this->generator->getModifyDefaultSQL($xmldb_table, $xmldb_field)) {
655 return; //Empty array = nothing to do = no error
658 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
662 * This function will rename the field in the table passed as arguments
663 * Before renaming the field, the function will check it exists
665 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
666 * @param xmldb_field $xmldb_field Index object (full specs are required).
667 * @param string $newname New name of the field.
668 * @return void
670 public function rename_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field, $newname) {
671 if (empty($newname)) {
672 throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
675 if (!$this->table_exists($xmldb_table)) {
676 throw new ddl_table_missing_exception($xmldb_table->getName());
679 // Check the field exists
680 if (!$this->field_exists($xmldb_table, $xmldb_field)) {
681 throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
684 // Check we have included full field specs
685 if (!$xmldb_field->getType()) {
686 throw new ddl_exception('ddlunknownerror', null,
687 'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .
688 ' must contain full specs. Rename skipped');
691 // Check field isn't id. Renaming over that field is not allowed
692 if ($xmldb_field->getName() == 'id') {
693 throw new ddl_exception('ddlunknownerror', null,
694 'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .
695 ' cannot be renamed. Rename skipped');
698 if (!$sqlarr = $this->generator->getRenameFieldSQL($xmldb_table, $xmldb_field, $newname)) {
699 return; //Empty array = nothing to do = no error
702 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
706 * This function will check, for the given table and field, if there there is any dependency
707 * preventing the field to be modified. It's used by all the public methods that perform any
708 * DDL change on fields, throwing one ddl_dependency_exception if dependencies are found.
710 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
711 * @param xmldb_field $xmldb_field Index object (full specs are required).
712 * @return void
713 * @throws ddl_dependency_exception|ddl_field_missing_exception|ddl_table_missing_exception if dependency not met.
715 private function check_field_dependencies(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
717 // Check the table exists
718 if (!$this->table_exists($xmldb_table)) {
719 throw new ddl_table_missing_exception($xmldb_table->getName());
722 // Check the field exists
723 if (!$this->field_exists($xmldb_table, $xmldb_field)) {
724 throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
727 // Check the field isn't in use by any index in the table
728 if ($indexes = $this->mdb->get_indexes($xmldb_table->getName(), false)) {
729 foreach ($indexes as $indexname => $index) {
730 $columns = $index['columns'];
731 if (in_array($xmldb_field->getName(), $columns)) {
732 throw new ddl_dependency_exception('column', $xmldb_table->getName() . '->' . $xmldb_field->getName(),
733 'index', $indexname . ' (' . implode(', ', $columns) . ')');
740 * This function will create the key in the table passed as arguments
742 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
743 * @param xmldb_key $xmldb_key Index object (full specs are required).
744 * @return void
746 public function add_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {
748 if ($xmldb_key->getType() == XMLDB_KEY_PRIMARY) { // Prevent PRIMARY to be added (only in create table, being serious :-P)
749 throw new ddl_exception('ddlunknownerror', null, 'Primary Keys can be added at table create time only');
752 if (!$sqlarr = $this->generator->getAddKeySQL($xmldb_table, $xmldb_key)) {
753 return; //Empty array = nothing to do = no error
756 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
760 * This function will drop the key in the table passed as arguments
762 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
763 * @param xmldb_key $xmldb_key Key object (full specs are required).
764 * @return void
766 public function drop_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {
767 if ($xmldb_key->getType() == XMLDB_KEY_PRIMARY) { // Prevent PRIMARY to be dropped (only in drop table, being serious :-P)
768 throw new ddl_exception('ddlunknownerror', null, 'Primary Keys can be deleted at table drop time only');
771 if (!$sqlarr = $this->generator->getDropKeySQL($xmldb_table, $xmldb_key)) {
772 return; //Empty array = nothing to do = no error
775 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
779 * This function will rename the key in the table passed as arguments
780 * Experimental. Shouldn't be used at all in normal installation/upgrade!
782 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
783 * @param xmldb_key $xmldb_key key object (full specs are required).
784 * @param string $newname New name of the key.
785 * @return void
787 public function rename_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key, $newname) {
788 debugging('rename_key() is one experimental feature. You must not use it in production!', DEBUG_DEVELOPER);
790 // Check newname isn't empty
791 if (!$newname) {
792 throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
795 if (!$sqlarr = $this->generator->getRenameKeySQL($xmldb_table, $xmldb_key, $newname)) {
796 throw new ddl_exception('ddlunknownerror', null, 'Some DBs do not support key renaming (MySQL, PostgreSQL, MsSQL). Rename skipped');
799 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
803 * This function will create the index in the table passed as arguments
804 * Before creating the index, the function will check it doesn't exists
806 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
807 * @param xmldb_index $xmldb_intex Index object (full specs are required).
808 * @return void
810 public function add_index($xmldb_table, $xmldb_intex) {
811 if (!$this->table_exists($xmldb_table)) {
812 throw new ddl_table_missing_exception($xmldb_table->getName());
815 // Check index doesn't exist
816 if ($this->index_exists($xmldb_table, $xmldb_intex)) {
817 throw new ddl_exception('ddlunknownerror', null,
818 'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .
819 ' already exists. Create skipped');
822 if (!$sqlarr = $this->generator->getAddIndexSQL($xmldb_table, $xmldb_intex)) {
823 throw new ddl_exception('ddlunknownerror', null, 'add_index sql not generated');
826 try {
827 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
828 } catch (ddl_change_structure_exception $e) {
829 // There could be a problem with the index length related to the row format of the table.
830 // If we are using utf8mb4 and the row format is 'compact' or 'redundant' then we need to change it over to
831 // 'compressed' or 'dynamic'.
832 if (method_exists($this->mdb, 'convert_table_row_format')) {
833 $this->mdb->convert_table_row_format($xmldb_table->getName());
834 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
835 } else {
836 // It's some other problem that we are currently not handling.
837 throw $e;
843 * This function will drop the index in the table passed as arguments
844 * Before dropping the index, the function will check it exists
846 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
847 * @param xmldb_index $xmldb_intex Index object (full specs are required).
848 * @return void
850 public function drop_index($xmldb_table, $xmldb_intex) {
851 if (!$this->table_exists($xmldb_table)) {
852 throw new ddl_table_missing_exception($xmldb_table->getName());
855 // Check index exists
856 if (!$this->index_exists($xmldb_table, $xmldb_intex)) {
857 throw new ddl_exception('ddlunknownerror', null,
858 'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .
859 ' does not exist. Drop skipped');
862 if (!$sqlarr = $this->generator->getDropIndexSQL($xmldb_table, $xmldb_intex)) {
863 throw new ddl_exception('ddlunknownerror', null, 'drop_index sql not generated');
866 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
870 * This function will rename the index in the table passed as arguments
871 * Before renaming the index, the function will check it exists
872 * Experimental. Shouldn't be used at all!
874 * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
875 * @param xmldb_index $xmldb_intex Index object (full specs are required).
876 * @param string $newname New name of the index.
877 * @return void
879 public function rename_index($xmldb_table, $xmldb_intex, $newname) {
880 debugging('rename_index() is one experimental feature. You must not use it in production!', DEBUG_DEVELOPER);
882 // Check newname isn't empty
883 if (!$newname) {
884 throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
887 // Check index exists
888 if (!$this->index_exists($xmldb_table, $xmldb_intex)) {
889 throw new ddl_exception('ddlunknownerror', null,
890 'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .
891 ' does not exist. Rename skipped');
894 if (!$sqlarr = $this->generator->getRenameIndexSQL($xmldb_table, $xmldb_intex, $newname)) {
895 throw new ddl_exception('ddlunknownerror', null, 'Some DBs do not support index renaming (MySQL). Rename skipped');
898 $this->execute_sql_arr($sqlarr, array($xmldb_table->getName()));
902 * Get the list of install.xml files.
904 * @return array
906 public function get_install_xml_files(): array {
907 global $CFG;
908 require_once($CFG->libdir.'/adminlib.php');
910 $files = [];
911 $dbdirs = get_db_directories();
912 foreach ($dbdirs as $dbdir) {
913 $filename = "{$dbdir}/install.xml";
914 if (file_exists($filename)) {
915 $files[] = $filename;
919 return $files;
923 * Reads the install.xml files for Moodle core and modules and returns an array of
924 * xmldb_structure object with xmldb_table from these files.
925 * @return xmldb_structure schema from install.xml files
927 public function get_install_xml_schema() {
928 global $CFG;
929 require_once($CFG->libdir.'/adminlib.php');
931 $schema = new xmldb_structure('export');
932 $schema->setVersion($CFG->version);
934 foreach ($this->get_install_xml_files() as $filename) {
935 $xmldb_file = new xmldb_file($filename);
936 if (!$xmldb_file->loadXMLStructure()) {
937 continue;
939 $structure = $xmldb_file->getStructure();
940 $tables = $structure->getTables();
941 foreach ($tables as $table) {
942 $table->setPrevious(null);
943 $table->setNext(null);
944 $schema->addTable($table);
947 return $schema;
951 * Checks the database schema against a schema specified by an xmldb_structure object
952 * @param xmldb_structure $schema export schema describing all known tables
953 * @param array $options
954 * @return array keyed by table name with array of difference messages as values
956 public function check_database_schema(xmldb_structure $schema, array $options = null) {
957 $alloptions = array(
958 'extratables' => true,
959 'missingtables' => true,
960 'extracolumns' => true,
961 'missingcolumns' => true,
962 'changedcolumns' => true,
965 $typesmap = array(
966 'I' => XMLDB_TYPE_INTEGER,
967 'R' => XMLDB_TYPE_INTEGER,
968 'N' => XMLDB_TYPE_NUMBER,
969 'F' => XMLDB_TYPE_NUMBER, // Nobody should be using floats!
970 'C' => XMLDB_TYPE_CHAR,
971 'X' => XMLDB_TYPE_TEXT,
972 'B' => XMLDB_TYPE_BINARY,
973 'T' => XMLDB_TYPE_TIMESTAMP,
974 'D' => XMLDB_TYPE_DATETIME,
977 $options = (array)$options;
978 $options = array_merge($alloptions, $options);
980 // Note: the error descriptions are not supposed to be localised,
981 // it is intended for developers and skilled admins only.
982 $errors = array();
984 /** @var string[] $dbtables */
985 $dbtables = $this->mdb->get_tables(false);
986 /** @var xmldb_table[] $tables */
987 $tables = $schema->getTables();
989 foreach ($tables as $table) {
990 $tablename = $table->getName();
992 if ($options['missingtables']) {
993 // Missing tables are a fatal problem.
994 if (empty($dbtables[$tablename])) {
995 $errors[$tablename][] = "table is missing";
996 continue;
1000 /** @var database_column_info[] $dbfields */
1001 $dbfields = $this->mdb->get_columns($tablename, false);
1002 /** @var xmldb_field[] $fields */
1003 $fields = $table->getFields();
1005 foreach ($fields as $field) {
1006 $fieldname = $field->getName();
1007 if (empty($dbfields[$fieldname])) {
1008 if ($options['missingcolumns']) {
1009 // Missing columns are a fatal problem.
1010 $errors[$tablename][] = "column '$fieldname' is missing";
1012 } else if ($options['changedcolumns']) {
1013 $dbfield = $dbfields[$fieldname];
1015 if (!isset($typesmap[$dbfield->meta_type])) {
1016 $errors[$tablename][] = "column '$fieldname' has unsupported type '$dbfield->meta_type'";
1017 } else {
1018 $dbtype = $typesmap[$dbfield->meta_type];
1019 $type = $field->getType();
1020 if ($type == XMLDB_TYPE_FLOAT) {
1021 $type = XMLDB_TYPE_NUMBER;
1023 if ($type != $dbtype) {
1024 if ($expected = array_search($type, $typesmap)) {
1025 $errors[$tablename][] = "column '$fieldname' has incorrect type '$dbfield->meta_type', expected '$expected'";
1026 } else {
1027 $errors[$tablename][] = "column '$fieldname' has incorrect type '$dbfield->meta_type'";
1029 } else {
1030 if ($field->getNotNull() != $dbfield->not_null) {
1031 if ($field->getNotNull()) {
1032 $errors[$tablename][] = "column '$fieldname' should be NOT NULL ($dbfield->meta_type)";
1033 } else {
1034 $errors[$tablename][] = "column '$fieldname' should allow NULL ($dbfield->meta_type)";
1037 if ($dbtype == XMLDB_TYPE_TEXT) {
1038 // No length check necessary - there is one size only now.
1040 } else if ($dbtype == XMLDB_TYPE_NUMBER) {
1041 if ($field->getType() == XMLDB_TYPE_FLOAT) {
1042 // Do not use floats in any new code, they are deprecated in XMLDB editor!
1044 } else if ($field->getLength() != $dbfield->max_length or $field->getDecimals() != $dbfield->scale) {
1045 $size = "({$field->getLength()},{$field->getDecimals()})";
1046 $dbsize = "($dbfield->max_length,$dbfield->scale)";
1047 $errors[$tablename][] = "column '$fieldname' size is $dbsize, expected $size ($dbfield->meta_type)";
1050 } else if ($dbtype == XMLDB_TYPE_CHAR) {
1051 // This is not critical, but they should ideally match.
1052 if ($field->getLength() != $dbfield->max_length) {
1053 $errors[$tablename][] = "column '$fieldname' length is $dbfield->max_length, expected {$field->getLength()} ($dbfield->meta_type)";
1056 } else if ($dbtype == XMLDB_TYPE_INTEGER) {
1057 // Integers may be bigger in some DBs.
1058 $length = $field->getLength();
1059 if ($length > 18) {
1060 // Integers are not supposed to be bigger than 18.
1061 $length = 18;
1063 if ($length > $dbfield->max_length) {
1064 $errors[$tablename][] = "column '$fieldname' length is $dbfield->max_length, expected at least {$field->getLength()} ($dbfield->meta_type)";
1067 } else if ($dbtype == XMLDB_TYPE_BINARY) {
1068 // Ignore binary types.
1069 continue;
1071 } else if ($dbtype == XMLDB_TYPE_TIMESTAMP) {
1072 $errors[$tablename][] = "column '$fieldname' is a timestamp, this type is not supported ($dbfield->meta_type)";
1073 continue;
1075 } else if ($dbtype == XMLDB_TYPE_DATETIME) {
1076 $errors[$tablename][] = "column '$fieldname' is a datetime, this type is not supported ($dbfield->meta_type)";
1077 continue;
1079 } else {
1080 // Report all other unsupported types as problems.
1081 $errors[$tablename][] = "column '$fieldname' has unknown type ($dbfield->meta_type)";
1082 continue;
1085 // Note: The empty string defaults are a bit messy...
1086 if ($field->getDefault() != $dbfield->default_value) {
1087 $default = is_null($field->getDefault()) ? 'NULL' : $field->getDefault();
1088 $dbdefault = is_null($dbfield->default_value) ? 'NULL' : $dbfield->default_value;
1089 $errors[$tablename][] = "column '$fieldname' has default '$dbdefault', expected '$default' ($dbfield->meta_type)";
1094 unset($dbfields[$fieldname]);
1097 // Check for extra columns (indicates unsupported hacks) - modify install.xml if you want to pass validation.
1098 foreach ($dbfields as $fieldname => $dbfield) {
1099 if ($options['extracolumns']) {
1100 $errors[$tablename][] = "column '$fieldname' is not expected ($dbfield->meta_type)";
1103 unset($dbtables[$tablename]);
1106 if ($options['extratables']) {
1107 // Look for unsupported tables - local custom tables should be in /local/xxxx/db/install.xml file.
1108 // If there is no prefix, we can not say if table is ours, sorry.
1109 if ($this->generator->prefix !== '') {
1110 foreach ($dbtables as $tablename => $unused) {
1111 if (strpos($tablename, 'pma_') === 0) {
1112 // Ignore phpmyadmin tables.
1113 continue;
1115 if (strpos($tablename, 'test') === 0) {
1116 // Legacy simple test db tables need to be eventually removed,
1117 // report them as problems!
1118 $errors[$tablename][] = "table is not expected (it may be a leftover after Simpletest unit tests)";
1119 } else {
1120 $errors[$tablename][] = "table is not expected";
1126 return $errors;