Merge branch 's17_MDL-30984_comment_docblock_master' of git://github.com/dongsheng...
[moodle.git] / lib / ddl / database_manager.php
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/>.
18 /**
19  * Database manager instance is responsible for all database structure modifications.
20  *
21  * @package    core
22  * @category   ddl
23  * @subpackage ddl
24  * @copyright  1999 onwards Martin Dougiamas     http://dougiamas.com
25  *             2001-3001 Eloy Lafuente (stronk7) http://contiento.com
26  *             2008 Petr Skoda                   http://skodak.org
27  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28  */
30 defined('MOODLE_INTERNAL') || die();
32 /**
33  * Database manager instance is responsible for all database structure modifications.
34  *
35  * It is using db specific generators to find out the correct SQL syntax to do that.
36  *
37  * @package    core
38  * @category   ddl
39  * @subpackage ddl
40  * @copyright  1999 onwards Martin Dougiamas     http://dougiamas.com
41  *             2001-3001 Eloy Lafuente (stronk7) http://contiento.com
42  *             2008 Petr Skoda                   http://skodak.org
43  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44  */
45 class database_manager {
47     /** @var moodle_database A moodle_database driver speific instance.*/
48     protected $mdb;
49     /** @var sql_generator A driver specific SQL generator instance. Public because XMLDB editor needs to access it.*/
50     public $generator;
52     /**
53      * Creates a new database manager instance.
54      * @param moodle_database $mdb A moodle_database driver specific instance.
55      * @param sql_generator $generator A driver specific SQL generator instance.
56      */
57     public function __construct($mdb, $generator) {
58         global $CFG;
60         $this->mdb       = $mdb;
61         $this->generator = $generator;
62     }
64     /**
65      * Releases all resources
66      */
67     public function dispose() {
68         if ($this->generator) {
69             $this->generator->dispose();
70             $this->generator = null;
71         }
72         $this->mdb = null;
73     }
75     /**
76      * This function will execute an array of SQL commands.
77      *
78      * @param array $sqlarr Array of sql statements to execute.
79      * @throws ddl_exception This exception is thrown if any error is found.
80      */
81     protected function execute_sql_arr(array $sqlarr) {
82         foreach ($sqlarr as $sql) {
83             $this->execute_sql($sql);
84         }
85     }
87     /**
88      * Execute a given sql command string.
89      *
90      * @param string $sql The sql string you wish to be executed.
91      * @throws ddl_exception This exception is thrown if any error is found.
92      */
93     protected function execute_sql($sql) {
94         if (!$this->mdb->change_database_structure($sql)) {
95             // in case driver does not throw exceptions yet ;-)
96             throw new ddl_change_structure_exception($this->mdb->get_last_error(), $sql);
97         }
98     }
100     /**
101      * Given one xmldb_table, check if it exists in DB (true/false).
102      *
103      * @param mixed $table The table to be searched (string name or xmldb_table instance).
104      * @return bool true/false True is a table exists, false otherwise.
105      */
106     public function table_exists($table) {
107         if (!is_string($table) and !($table instanceof xmldb_table)) {
108             throw new ddl_exception('ddlunknownerror', NULL, 'incorrect table parameter!');
109         }
110         return $this->generator->table_exists($table);
111     }
113     /**
114      * Reset a sequence to the id field of a table.
115      * @param string $table Name of table.
116      * @throws ddl_exception|ddl_table_missing_exception Exception thrown upon reset errors.
117      */
118     public function reset_sequence($table) {
119         if (!is_string($table) and !($table instanceof xmldb_table)) {
120             throw new ddl_exception('ddlunknownerror', NULL, 'incorrect table parameter!');
121         }
123     /// Check the table exists
124         if (!$this->table_exists($table)) {
125             throw new ddl_table_missing_exception($table);
126         }
128         if (!$sqlarr = $this->generator->getResetSequenceSQL($table)) {
129             throw new ddl_exception('ddlunknownerror', null, 'table reset sequence sql not generated');
130         }
132         $this->execute_sql_arr($sqlarr);
133     }
135     /**
136      * Given one xmldb_field, check if it exists in DB (true/false).
137      *
138      * @param mixed $table The table to be searched (string name or xmldb_table instance).
139      * @param mixed $field The field to be searched for (string name or xmldb_field instance).
140      * @return boolean true is exists false otherwise.
141      * @throws ddl_table_missing_exception
142      */
143     public function field_exists($table, $field) {
144     /// Calculate the name of the table
145         if (is_string($table)) {
146             $tablename = $table;
147         } else {
148             $tablename = $table->getName();
149         }
151     /// Check the table exists
152         if (!$this->table_exists($table)) {
153             throw new ddl_table_missing_exception($tablename);
154         }
156         if (is_string($field)) {
157             $fieldname = $field;
158         } else {
159         /// Calculate the name of the table
160             $fieldname = $field->getName();
161         }
163     /// Get list of fields in table
164         $columns = $this->mdb->get_columns($tablename);
166         $exists = array_key_exists($fieldname,  $columns);
168         return $exists;
169     }
171     /**
172      * Given one xmldb_index, the function returns the name of the index in DB
173      * of false if it doesn't exist
174      *
175      * @param xmldb_table $xmldb_table table to be searched
176      * @param xmldb_index $xmldb_index the index to be searched
177      * @return string|bool Index name or false if no indexes are found.
178      * @throws ddl_table_missing_exception Thrown when table is not found.
179      */
180     public function find_index_name(xmldb_table $xmldb_table, xmldb_index $xmldb_index) {
181     /// Calculate the name of the table
182         $tablename = $xmldb_table->getName();
184     /// Check the table exists
185         if (!$this->table_exists($xmldb_table)) {
186             throw new ddl_table_missing_exception($tablename);
187         }
189     /// Extract index columns
190         $indcolumns = $xmldb_index->getFields();
192     /// Get list of indexes in table
193         $indexes = $this->mdb->get_indexes($tablename);
195     /// Iterate over them looking for columns coincidence
196         foreach ($indexes as $indexname => $index) {
197             $columns = $index['columns'];
198         /// Check if index matchs queried index
199             $diferences = array_merge(array_diff($columns, $indcolumns), array_diff($indcolumns, $columns));
200         /// If no diferences, we have find the index
201             if (empty($diferences)) {
202                 return $indexname;
203             }
204         }
206     /// Arriving here, index not found
207         return false;
208     }
210     /**
211      * Given one xmldb_index, check if it exists in DB (true/false).
212      *
213      * @param xmldb_table $xmldb_table The table to be searched.
214      * @param xmldb_index $xmldb_index The index to be searched for.
215      * @return boolean true id index exists, false otherwise.
216      */
217     public function index_exists(xmldb_table $xmldb_table, xmldb_index $xmldb_index) {
218         if (!$this->table_exists($xmldb_table)) {
219             return false;
220         }
221         return ($this->find_index_name($xmldb_table, $xmldb_index) !== false);
222     }
224     /**
225      * Given one xmldb_field, the function returns the name of the check constraint in DB (if exists)
226      * of false if it doesn't exist. Note that XMLDB limits the number of check constraints per field
227      * to 1 "enum-like" constraint. So, if more than one is returned, only the first one will be
228      * retrieved by this function.
229      *
230      * @todo MDL-31147 Moodle 2.1 - Drop find_check_constraint_name()
231      *
232      * @param xmldb_table $xmldb_table The table to be searched.
233      * @param xmldb_field $xmldb_field The field to be searched.
234      * @return string|bool check constraint name or false
235      */
236     public function find_check_constraint_name(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
238     /// Check the table exists
239         if (!$this->table_exists($xmldb_table)) {
240             throw new ddl_table_missing_exception($xmldb_table->getName());
241         }
243     /// Check the field exists
244         if (!$this->field_exists($xmldb_table, $xmldb_field)) {
245             throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
246         }
248     /// Get list of check_constraints in table/field
249         $checks = false;
250         if ($objchecks = $this->generator->getCheckConstraintsFromDB($xmldb_table, $xmldb_field)) {
251         /// Get only the 1st element. Shouldn't be more than 1 under XMLDB
252             $objcheck = array_shift($objchecks);
253             if ($objcheck) {
254                 $checks = strtolower($objcheck->name);
255             }
256         }
258     /// Arriving here, check not found
259         return $checks;
260     }
262     /**
263      * Given one xmldb_field, check if it has a check constraint in DB
264      *
265      * TODO: Moodle 2.1 - Drop check_constraint_exists()
266      *
267      * @param xmldb_table $xmldb_table The table.
268      * @param xmldb_field $xmldb_field The field to be searched for any existing constraint.
269      * @return boolean true/false
270      */
271     public function check_constraint_exists(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
272         return ($this->find_check_constraint_name($xmldb_table, $xmldb_field) !== false);
273     }
275     /**
276      * This function IS NOT IMPLEMENTED. ONCE WE'LL BE USING RELATIONAL
277      * INTEGRITY IT WILL BECOME MORE USEFUL. FOR NOW, JUST CALCULATE "OFFICIAL"
278      * KEY NAMES WITHOUT ACCESSING TO DB AT ALL.
279      * Given one xmldb_key, the function returns the name of the key in DB (if exists)
280      * of false if it doesn't exist
281      *
282      * @param xmldb_table $xmldb_table The table to be searched.
283      * @param xmldb_key $xmldb_key The key to be searched.
284      * @return string key name if found
285      */
286     public function find_key_name(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {
288         $keycolumns = $xmldb_key->getFields();
290     /// Get list of keys in table
291     /// first primaries (we aren't going to use this now, because the MetaPrimaryKeys is awful)
292         ///TODO: To implement when we advance in relational integrity
293     /// then uniques (note that Moodle, for now, shouldn't have any UNIQUE KEY for now, but unique indexes)
294         ///TODO: To implement when we advance in relational integrity (note that AdoDB hasn't any MetaXXX for this.
295     /// then foreign (note that Moodle, for now, shouldn't have any FOREIGN KEY for now, but indexes)
296         ///TODO: To implement when we advance in relational integrity (note that AdoDB has one MetaForeignKeys()
297         ///but it's far from perfect.
298     /// TODO: To create the proper functions inside each generator to retrieve all the needed KEY info (name
299     ///       columns, reftable and refcolumns
301     /// So all we do is to return the official name of the requested key without any confirmation!)
302     /// One exception, hardcoded primary constraint names
303         if ($this->generator->primary_key_name && $xmldb_key->getType() == XMLDB_KEY_PRIMARY) {
304             return $this->generator->primary_key_name;
305         } else {
306         /// Calculate the name suffix
307             switch ($xmldb_key->getType()) {
308                 case XMLDB_KEY_PRIMARY:
309                     $suffix = 'pk';
310                     break;
311                 case XMLDB_KEY_UNIQUE:
312                     $suffix = 'uk';
313                     break;
314                 case XMLDB_KEY_FOREIGN_UNIQUE:
315                 case XMLDB_KEY_FOREIGN:
316                     $suffix = 'fk';
317                     break;
318             }
319         /// And simply, return the official name
320             return $this->generator->getNameForObject($xmldb_table->getName(), implode(', ', $xmldb_key->getFields()), $suffix);
321         }
322     }
324     /**
325      * This function will delete all tables found in XMLDB file from db
326      *
327      * @param string $file Full path to the XML file to be used.
328      * @return void
329      */
330     public function delete_tables_from_xmldb_file($file) {
332         $xmldb_file = new xmldb_file($file);
334         if (!$xmldb_file->fileExists()) {
335             throw new ddl_exception('ddlxmlfileerror', null, 'File does not exist');
336         }
338         $loaded    = $xmldb_file->loadXMLStructure();
339         $structure = $xmldb_file->getStructure();
341         if (!$loaded || !$xmldb_file->isLoaded()) {
342         /// Show info about the error if we can find it
343             if ($structure) {
344                 if ($errors = $structure->getAllErrors()) {
345                     throw new ddl_exception('ddlxmlfileerror', null, 'Errors found in XMLDB file: '. implode (', ', $errors));
346                 }
347             }
348             throw new ddl_exception('ddlxmlfileerror', null, 'not loaded??');
349         }
351         if ($xmldb_tables = $structure->getTables()) {
352             foreach($xmldb_tables as $table) {
353                 if ($this->table_exists($table)) {
354                     $this->drop_table($table);
355                 }
356             }
357         }
358     }
360     /**
361      * This function will drop the table passed as argument
362      * and all the associated objects (keys, indexes, constraints, sequences, triggers)
363      * will be dropped too.
364      *
365      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
366      * @return void
367      */
368     public function drop_table(xmldb_table $xmldb_table) {
369     /// Check table exists
370         if (!$this->table_exists($xmldb_table)) {
371             throw new ddl_table_missing_exception($xmldb_table->getName());
372         }
374         if (!$sqlarr = $this->generator->getDropTableSQL($xmldb_table)) {
375             throw new ddl_exception('ddlunknownerror', null, 'table drop sql not generated');
376         }
378         $this->execute_sql_arr($sqlarr);
379     }
381     /**
382      * Load an install.xml file, checking that it exists, and that the structure is OK.
383      * @param string $file the full path to the XMLDB file.
384      * @return xmldb_file the loaded file.
385      */
386     private function load_xmldb_file($file) {
387         $xmldb_file = new xmldb_file($file);
389         if (!$xmldb_file->fileExists()) {
390             throw new ddl_exception('ddlxmlfileerror', null, 'File does not exist');
391         }
393         $loaded = $xmldb_file->loadXMLStructure();
394         if (!$loaded || !$xmldb_file->isLoaded()) {
395         /// Show info about the error if we can find it
396             if ($structure =& $xmldb_file->getStructure()) {
397                 if ($errors = $structure->getAllErrors()) {
398                     throw new ddl_exception('ddlxmlfileerror', null, 'Errors found in XMLDB file: '. implode (', ', $errors));
399                 }
400             }
401             throw new ddl_exception('ddlxmlfileerror', null, 'not loaded??');
402         }
404         return $xmldb_file;
405     }
407     /**
408      * This function will load one entire XMLDB file and call install_from_xmldb_structure.
409      *
410      * @param string $file full path to the XML file to be used
411      * @return void
412      */
413     public function install_from_xmldb_file($file) {
414         $xmldb_file = $this->load_xmldb_file($file);
415         $xmldb_structure = $xmldb_file->getStructure();
416         $this->install_from_xmldb_structure($xmldb_structure);
417     }
419     /**
420      * This function will load one entire XMLDB file and call install_from_xmldb_structure.
421      *
422      * @param string $file full path to the XML file to be used
423      * @param string $tablename the name of the table.
424      * @param bool $cachestructures boolean to decide if loaded xmldb structures can be safely cached
425      *             useful for testunits loading the enormous main xml file hundred of times (100x)
426      */
427     public function install_one_table_from_xmldb_file($file, $tablename, $cachestructures = false) {
429         static $xmldbstructurecache = array(); // To store cached structures
430         if (!empty($xmldbstructurecache) && array_key_exists($file, $xmldbstructurecache)) {
431             $xmldb_structure = $xmldbstructurecache[$file];
432         } else {
433             $xmldb_file = $this->load_xmldb_file($file);
434             $xmldb_structure = $xmldb_file->getStructure();
435             if ($cachestructures) {
436                 $xmldbstructurecache[$file] = $xmldb_structure;
437             }
438         }
440         $targettable = $xmldb_structure->getTable($tablename);
441         if (is_null($targettable)) {
442             throw new ddl_exception('ddlunknowntable', null, 'The table ' . $tablename . ' is not defined in file ' . $file);
443         }
444         $targettable->setNext(NULL);
445         $targettable->setPrevious(NULL);
447         $tempstructure = new xmldb_structure('temp');
448         $tempstructure->addTable($targettable);
449         $this->install_from_xmldb_structure($tempstructure);
450     }
452     /**
453      * This function will generate all the needed SQL statements, specific for each
454      * RDBMS type and, finally, it will execute all those statements against the DB.
455      *
456      * @param stdClass $xmldb_structure xmldb_structure object.
457      * @return void
458      */
459     public function install_from_xmldb_structure($xmldb_structure) {
461         if (!$sqlarr = $this->generator->getCreateStructureSQL($xmldb_structure)) {
462             return; // nothing to do
463         }
464         $this->execute_sql_arr($sqlarr);
465     }
467     /**
468      * This function will create the table passed as argument with all its
469      * fields/keys/indexes/sequences, everything based in the XMLDB object
470      *
471      * @param xmldb_table $xmldb_table Table object (full specs are required).
472      * @return void
473      */
474     public function create_table(xmldb_table $xmldb_table) {
475     /// Check table doesn't exist
476         if ($this->table_exists($xmldb_table)) {
477             throw new ddl_exception('ddltablealreadyexists', $xmldb_table->getName());
478         }
480         if (!$sqlarr = $this->generator->getCreateTableSQL($xmldb_table)) {
481             throw new ddl_exception('ddlunknownerror', null, 'table create sql not generated');
482         }
483         $this->execute_sql_arr($sqlarr);
484     }
486     /**
487      * This function will create the temporary table passed as argument with all its
488      * fields/keys/indexes/sequences, everything based in the XMLDB object
489      *
490      * If table already exists ddl_exception will be thrown, please make sure
491      * the table name does not collide with existing normal table!
492      *
493      * @param xmldb_table $xmldb_table Table object (full specs are required).
494      * @return void
495      */
496     public function create_temp_table(xmldb_table $xmldb_table) {
498         // Check table doesn't exist
499         if ($this->table_exists($xmldb_table)) {
500             throw new ddl_exception('ddltablealreadyexists', $xmldb_table->getName());
501         }
503         if (!$sqlarr = $this->generator->getCreateTempTableSQL($xmldb_table)) {
504             throw new ddl_exception('ddlunknownerror', null, 'temp table create sql not generated');
505         }
507         $this->execute_sql_arr($sqlarr);
508     }
510     /**
511      * This function will drop the temporary table passed as argument with all its
512      * fields/keys/indexes/sequences, everything based in the XMLDB object
513      *
514      * It is recommended to drop temp table when not used anymore.
515      *
516      * @param xmldb_table $xmldb_table Table object.
517      * @return void
518      */
519     public function drop_temp_table(xmldb_table $xmldb_table) {
521     /// Check table doesn't exist
522         if (!$this->table_exists($xmldb_table)) {
523             throw new ddl_table_missing_exception($xmldb_table->getName());
524         }
526         if (!$sqlarr = $this->generator->getDropTempTableSQL($xmldb_table)) {
527             throw new ddl_exception('ddlunknownerror', null, 'temp table drop sql not generated');
528         }
530         $this->execute_sql_arr($sqlarr);
531     }
533     /**
534      * This function will rename the table passed as argument
535      * Before renaming the index, the function will check it exists
536      *
537      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
538      * @param string $newname New name of the index.
539      * @return void
540      */
541     public function rename_table(xmldb_table $xmldb_table, $newname) {
542     /// Check newname isn't empty
543         if (!$newname) {
544             throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
545         }
547         $check = new xmldb_table($newname);
549     /// Check table already renamed
550         if (!$this->table_exists($xmldb_table)) {
551             if ($this->table_exists($check)) {
552                 throw new ddl_exception('ddlunknownerror', null, 'table probably already renamed');
553             } else {
554                 throw new ddl_table_missing_exception($xmldb_table->getName());
555             }
556         }
558     /// Check new table doesn't exist
559         if ($this->table_exists($check)) {
560             throw new ddl_exception('ddltablealreadyexists', $xmldb_table->getName(), 'can not rename table');
561         }
563         if (!$sqlarr = $this->generator->getRenameTableSQL($xmldb_table, $newname)) {
564             throw new ddl_exception('ddlunknownerror', null, 'table rename sql not generated');
565         }
567         $this->execute_sql_arr($sqlarr);
568     }
571     /**
572      * This function will add the field to the table passed as arguments
573      *
574      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
575      * @param xmldb_field $xmldb_field Index object (full specs are required).
576      * @return void
577      */
578     public function add_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
579      /// Check the field doesn't exist
580         if ($this->field_exists($xmldb_table, $xmldb_field)) {
581             throw new ddl_exception('ddlfieldalreadyexists', $xmldb_field->getName());
582         }
584     /// If NOT NULL and no default given (we ask the generator about the
585     /// *real* default that will be used) check the table is empty
586         if ($xmldb_field->getNotNull() && $this->generator->getDefaultValue($xmldb_field) === NULL && $this->mdb->count_records($xmldb_table->getName())) {
587             throw new ddl_exception('ddlunknownerror', null, 'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .
588                       ' cannot be added. Not null fields added to non empty tables require default value. Create skipped');
589         }
591         if (!$sqlarr = $this->generator->getAddFieldSQL($xmldb_table, $xmldb_field)) {
592             throw new ddl_exception('ddlunknownerror', null, 'addfield sql not generated');
593         }
594         $this->execute_sql_arr($sqlarr);
595     }
597     /**
598      * This function will drop the field from the table passed as arguments
599      *
600      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
601      * @param xmldb_field $xmldb_field Index object (full specs are required).
602      * @return void
603      */
604     public function drop_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
605         if (!$this->table_exists($xmldb_table)) {
606             throw new ddl_table_missing_exception($xmldb_table->getName());
607         }
608     /// Check the field exists
609         if (!$this->field_exists($xmldb_table, $xmldb_field)) {
610             throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
611         }
612     /// Check for dependencies in the DB before performing any action
613         $this->check_field_dependencies($xmldb_table, $xmldb_field);
615         if (!$sqlarr = $this->generator->getDropFieldSQL($xmldb_table, $xmldb_field)) {
616             throw new ddl_exception('ddlunknownerror', null, 'drop_field sql not generated');
617         }
619         $this->execute_sql_arr($sqlarr);
620     }
622     /**
623      * This function will change the type of the field in the table passed as arguments
624      *
625      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
626      * @param xmldb_field $xmldb_field Index object (full specs are required).
627      * @return void
628      */
629     public function change_field_type(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
630         if (!$this->table_exists($xmldb_table)) {
631             throw new ddl_table_missing_exception($xmldb_table->getName());
632         }
633     /// Check the field exists
634         if (!$this->field_exists($xmldb_table, $xmldb_field)) {
635             throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
636         }
637     /// Check for dependencies in the DB before performing any action
638         $this->check_field_dependencies($xmldb_table, $xmldb_field);
640         if (!$sqlarr = $this->generator->getAlterFieldSQL($xmldb_table, $xmldb_field)) {
641             return; // probably nothing to do
642         }
644         $this->execute_sql_arr($sqlarr);
645     }
647     /**
648      * This function will change the precision of the field in the table passed as arguments
649      *
650      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
651      * @param xmldb_field $xmldb_field Index object (full specs are required).
652      * @return void
653      */
654     public function change_field_precision(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
655     /// Just a wrapper over change_field_type. Does exactly the same processing
656         $this->change_field_type($xmldb_table, $xmldb_field);
657     }
659     /**
660      * This function will change the unsigned/signed of the field in the table passed as arguments
661      *
662      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
663      * @param xmldb_field $xmldb_field Index object (full specs are required).
664      * @return void
665      */
666     public function change_field_unsigned(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
667     /// Just a wrapper over change_field_type. Does exactly the same processing
668         $this->change_field_type($xmldb_table, $xmldb_field);
669     }
671     /**
672      * This function will change the nullability of the field in the table passed as arguments
673      *
674      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
675      * @param xmldb_field $xmldb_field Index object (full specs are required).
676      * @return void
677      */
678     public function change_field_notnull(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
679     /// Just a wrapper over change_field_type. Does exactly the same processing
680         $this->change_field_type($xmldb_table, $xmldb_field);
681     }
683     /**
684      * This function will change the default of the field in the table passed as arguments
685      * One null value in the default field means delete the default
686      *
687      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
688      * @param xmldb_field $xmldb_field Index object (full specs are required).
689      * @return void
690      */
691     public function change_field_default(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
692         if (!$this->table_exists($xmldb_table)) {
693             throw new ddl_table_missing_exception($xmldb_table->getName());
694         }
695     /// Check the field exists
696         if (!$this->field_exists($xmldb_table, $xmldb_field)) {
697             throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
698         }
699     /// Check for dependencies in the DB before performing any action
700         $this->check_field_dependencies($xmldb_table, $xmldb_field);
702         if (!$sqlarr = $this->generator->getModifyDefaultSQL($xmldb_table, $xmldb_field)) {
703             return; //Empty array = nothing to do = no error
704         }
706         $this->execute_sql_arr($sqlarr);
707     }
709     /**
710      * This function will drop the existing enum of the field in the table passed as arguments
711      *
712      * TODO: Moodle 2.1 - Drop drop_enum_from_field()
713      *
714      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
715      * @param xmldb_field $xmldb_field Index object (full specs are required).
716      * @return void
717      */
718     public function drop_enum_from_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
719         if (!$this->table_exists($xmldb_table)) {
720             throw new ddl_table_missing_exception($xmldb_table->getName());
721         }
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());
725         }
727         if (!$this->check_constraint_exists($xmldb_table, $xmldb_field)) {
728             debugging('Enum for ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .
729                       ' does not exist. Delete skipped', DEBUG_DEVELOPER);
730             return; //Enum does not exist, nothing to delete
731         }
733         if (!$sqlarr = $this->generator->getDropEnumSQL($xmldb_table, $xmldb_field)) {
734             return; //Empty array = nothing to do = no error
735         }
737         $this->execute_sql_arr($sqlarr);
738     }
740     /**
741      * This function will rename the field in the table passed as arguments
742      * Before renaming the field, the function will check it exists
743      *
744      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
745      * @param xmldb_field $xmldb_field Index object (full specs are required).
746      * @param string $newname New name of the field.
747      * @return void
748      */
749     public function rename_field(xmldb_table $xmldb_table, xmldb_field $xmldb_field, $newname) {
750         if (empty($newname)) {
751             throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
752         }
754         if (!$this->table_exists($xmldb_table)) {
755             throw new ddl_table_missing_exception($xmldb_table->getName());
756         }
758     /// Check the field exists
759         if (!$this->field_exists($xmldb_table, $xmldb_field)) {
760             throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
761         }
763     /// Check we have included full field specs
764         if (!$xmldb_field->getType()) {
765             throw new ddl_exception('ddlunknownerror', null,
766                       'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .
767                       ' must contain full specs. Rename skipped');
768         }
770     /// Check field isn't id. Renaming over that field is not allowed
771         if ($xmldb_field->getName() == 'id') {
772             throw new ddl_exception('ddlunknownerror', null,
773                       'Field ' . $xmldb_table->getName() . '->' . $xmldb_field->getName() .
774                       ' cannot be renamed. Rename skipped');
775         }
777         if (!$sqlarr = $this->generator->getRenameFieldSQL($xmldb_table, $xmldb_field, $newname)) {
778             return; //Empty array = nothing to do = no error
779         }
781         $this->execute_sql_arr($sqlarr);
782     }
784     /**
785      * This function will check, for the given table and field, if there there is any dependency
786      * preventing the field to be modified. It's used by all the public methods that perform any
787      * DDL change on fields, throwing one ddl_dependency_exception if dependencies are found.
788      *
789      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
790      * @param xmldb_field $xmldb_field Index object (full specs are required).
791      * @return void
792      * @throws ddl_dependency_exception|ddl_field_missing_exception|ddl_table_missing_exception if dependency not met.
793      */
794     private function check_field_dependencies(xmldb_table $xmldb_table, xmldb_field $xmldb_field) {
796     /// Check the table exists
797         if (!$this->table_exists($xmldb_table)) {
798             throw new ddl_table_missing_exception($xmldb_table->getName());
799         }
801     /// Check the field exists
802         if (!$this->field_exists($xmldb_table, $xmldb_field)) {
803             throw new ddl_field_missing_exception($xmldb_field->getName(), $xmldb_table->getName());
804         }
806     /// Check the field isn't in use by any index in the table
807         if ($indexes = $this->mdb->get_indexes($xmldb_table->getName(), false)) {
808             foreach ($indexes as $indexname => $index) {
809                 $columns = $index['columns'];
810                 if (in_array($xmldb_field->getName(), $columns)) {
811                     throw new ddl_dependency_exception('column', $xmldb_table->getName() . '->' . $xmldb_field->getName(),
812                                                        'index', $indexname . ' (' . implode(', ', $columns)  . ')');
813                 }
814             }
815         }
816     }
818     /**
819      * This function will create the key in the table passed as arguments
820      *
821      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
822      * @param xmldb_key $xmldb_key Index object (full specs are required).
823      * @return void
824      */
825     public function add_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {
827         if ($xmldb_key->getType() == XMLDB_KEY_PRIMARY) { // Prevent PRIMARY to be added (only in create table, being serious  :-P)
828             throw new ddl_exception('ddlunknownerror', null, 'Primary Keys can be added at table create time only');
829         }
831         if (!$sqlarr = $this->generator->getAddKeySQL($xmldb_table, $xmldb_key)) {
832             return; //Empty array = nothing to do = no error
833         }
835         $this->execute_sql_arr($sqlarr);
836     }
838     /**
839      * This function will drop the key in the table passed as arguments
840      *
841      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
842      * @param xmldb_key $xmldb_key Key object (full specs are required).
843      * @return void
844      */
845     public function drop_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key) {
846         if ($xmldb_key->getType() == XMLDB_KEY_PRIMARY) { // Prevent PRIMARY to be dropped (only in drop table, being serious  :-P)
847             throw new ddl_exception('ddlunknownerror', null, 'Primary Keys can be deleted at table drop time only');
848         }
850         if (!$sqlarr = $this->generator->getDropKeySQL($xmldb_table, $xmldb_key)) {
851             return; //Empty array = nothing to do = no error
852         }
854         $this->execute_sql_arr($sqlarr);
855     }
857     /**
858      * This function will rename the key in the table passed as arguments
859      * Experimental. Shouldn't be used at all in normal installation/upgrade!
860      *
861      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
862      * @param xmldb_key $xmldb_key key object (full specs are required).
863      * @param string $newname New name of the key.
864      * @return void
865      */
866     public function rename_key(xmldb_table $xmldb_table, xmldb_key $xmldb_key, $newname) {
867         debugging('rename_key() is one experimental feature. You must not use it in production!', DEBUG_DEVELOPER);
869     /// Check newname isn't empty
870         if (!$newname) {
871             throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
872         }
874         if (!$sqlarr = $this->generator->getRenameKeySQL($xmldb_table, $xmldb_key, $newname)) {
875             throw new ddl_exception('ddlunknownerror', null, 'Some DBs do not support key renaming (MySQL, PostgreSQL, MsSQL). Rename skipped');
876         }
878         $this->execute_sql_arr($sqlarr);
879     }
881     /**
882      * This function will create the index in the table passed as arguments
883      * Before creating the index, the function will check it doesn't exists
884      *
885      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
886      * @param xmldb_index $xmldb_intex Index object (full specs are required).
887      * @return void
888      */
889     public function add_index($xmldb_table, $xmldb_intex) {
890         if (!$this->table_exists($xmldb_table)) {
891             throw new ddl_table_missing_exception($xmldb_table->getName());
892         }
894     /// Check index doesn't exist
895         if ($this->index_exists($xmldb_table, $xmldb_intex)) {
896             throw new ddl_exception('ddlunknownerror', null,
897                       'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .
898                       ' already exists. Create skipped');
899         }
901         if (!$sqlarr = $this->generator->getAddIndexSQL($xmldb_table, $xmldb_intex)) {
902             throw new ddl_exception('ddlunknownerror', null, 'add_index sql not generated');
903         }
905         $this->execute_sql_arr($sqlarr);
906     }
908     /**
909      * This function will drop the index in the table passed as arguments
910      * Before dropping the index, the function will check it exists
911      *
912      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
913      * @param xmldb_index $xmldb_intex Index object (full specs are required).
914      * @return void
915      */
916     public function drop_index($xmldb_table, $xmldb_intex) {
917         if (!$this->table_exists($xmldb_table)) {
918             throw new ddl_table_missing_exception($xmldb_table->getName());
919         }
921     /// Check index exists
922         if (!$this->index_exists($xmldb_table, $xmldb_intex)) {
923             throw new ddl_exception('ddlunknownerror', null,
924                       'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .
925                       ' does not exist. Drop skipped');
926         }
928         if (!$sqlarr = $this->generator->getDropIndexSQL($xmldb_table, $xmldb_intex)) {
929             throw new ddl_exception('ddlunknownerror', null, 'drop_index sql not generated');
930         }
932         $this->execute_sql_arr($sqlarr);
933     }
935     /**
936      * This function will rename the index in the table passed as arguments
937      * Before renaming the index, the function will check it exists
938      * Experimental. Shouldn't be used at all!
939      *
940      * @param xmldb_table $xmldb_table Table object (just the name is mandatory).
941      * @param xmldb_index $xmldb_intex Index object (full specs are required).
942      * @param string $newname New name of the index.
943      * @return void
944      */
945     public function rename_index($xmldb_table, $xmldb_intex, $newname) {
946         debugging('rename_index() is one experimental feature. You must not use it in production!', DEBUG_DEVELOPER);
948     /// Check newname isn't empty
949         if (!$newname) {
950             throw new ddl_exception('ddlunknownerror', null, 'newname can not be empty');
951         }
953     /// Check index exists
954         if (!$this->index_exists($xmldb_table, $xmldb_intex)) {
955             throw new ddl_exception('ddlunknownerror', null,
956                       'Index ' . $xmldb_table->getName() . '->' . $xmldb_intex->getName() .
957                       ' does not exist. Rename skipped');
958         }
960         if (!$sqlarr = $this->generator->getRenameIndexSQL($xmldb_table, $xmldb_intex, $newname)) {
961             throw new ddl_exception('ddlunknownerror', null, 'Some DBs do not support index renaming (MySQL). Rename skipped');
962         }
964         $this->execute_sql_arr($sqlarr);
965     }
967     /**
968      * Reads the install.xml files for Moodle core and modules and returns an array of
969      * xmldb_structure object with xmldb_table from these files.
970      * @return xmldb_structure schema from install.xml files
971      */
972     public function get_install_xml_schema() {
973         global $CFG;
974         require_once($CFG->libdir.'/adminlib.php');
976         $schema = new xmldb_structure('export');
977         $schema->setVersion($CFG->version);
978         $dbdirs = get_db_directories();
979         foreach ($dbdirs as $dbdir) {
980             $xmldb_file = new xmldb_file($dbdir.'/install.xml');
981             if (!$xmldb_file->fileExists() or !$xmldb_file->loadXMLStructure()) {
982                 continue;
983             }
984             $structure = $xmldb_file->getStructure();
985             $tables = $structure->getTables();
986             foreach ($tables as $table) {
987                 $table->setPrevious(null);
988                 $table->setNext(null);
989                 $schema->addTable($table);
990             }
991         }
992         return $schema;
993     }
995     /**
996      * Checks the database schema against a schema specified by an xmldb_structure object
997      * @param xmldb_structure $schema export schema describing all known tables
998      * @return array keyed by table name with array of difference messages as values
999      */
1000     public function check_database_schema(xmldb_structure $schema) {
1001         $errors = array();
1003         $dbtables = $this->mdb->get_tables();
1004         $tables   = $schema->getTables();
1006         //TODO: maybe add several levels error/warning
1008         // make sure that current and schema tables match exactly
1009         foreach ($tables as $table) {
1010             $tablename = $table->getName();
1011             if (empty($dbtables[$tablename])) {
1012                 if (!isset($errors[$tablename])) {
1013                     $errors[$tablename] = array();
1014                 }
1015                 $errors[$tablename][] = "Table $tablename is missing in database."; //TODO: localize
1016                 continue;
1017             }
1019             // a) check for required fields
1020             $dbfields = $this->mdb->get_columns($tablename);
1021             $fields   = $table->getFields();
1022             foreach ($fields as $field) {
1023                 $fieldname = $field->getName();
1024                 if (empty($dbfields[$fieldname])) {
1025                     if (!isset($errors[$tablename])) {
1026                         $errors[$tablename] = array();
1027                     }
1028                     $errors[$tablename][] = "Field $fieldname is missing in table $tablename.";  //TODO: localize
1029                 }
1030                 unset($dbfields[$fieldname]);
1031             }
1033             // b) check for extra fields (indicates unsupported hacks) - modify install.xml if you want the script to continue ;-)
1034             foreach ($dbfields as $fieldname=>$info) {
1035                 if (!isset($errors[$tablename])) {
1036                     $errors[$tablename] = array();
1037                 }
1038                 $errors[$tablename][] = "Field $fieldname is not expected in table $tablename.";  //TODO: localize
1039             }
1040             unset($dbtables[$tablename]);
1041         }
1043         // look for unsupported tables - local custom tables should be in /local/xxxx/db/install.xml ;-)
1044         // if there is no prefix, we can not say if tale is ours :-(
1045         if ($this->generator->prefix !== '') {
1046             foreach ($dbtables as $tablename=>$unused) {
1047                 if (strpos($tablename, 'pma_') === 0) {
1048                     // ignore phpmyadmin tables for now
1049                     continue;
1050                 }
1051                 if (strpos($tablename, 'test') === 0) {
1052                     // ignore broken results of unit tests
1053                     continue;
1054                 }
1055                 if (!isset($errors[$tablename])) {
1056                     $errors[$tablename] = array();
1057                 }
1058                 $errors[$tablename][] = "Table $tablename is not expected.";  //TODO: localize
1059             }
1060         }
1062         return $errors;
1063     }