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