MDL-37726 stop using PREVIOUS/NEXT in install.xml files
[moodle.git] / lib / xmldb / xmldb_table.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/>.
17 /**
18  * This class represent one XMLDB table
19  *
20  * @package    core_xmldb
21  * @copyright  1999 onwards Martin Dougiamas     http://dougiamas.com
22  *             2001-3001 Eloy Lafuente (stronk7) http://contiento.com
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
29 class xmldb_table extends xmldb_object {
31     /** @var array table columns */
32     protected $fields;
34     /** @var array keys */
35     protected $keys;
37     /** @var array indexes */
38     protected $indexes;
40     /**
41      * Note:
42      *  - Oracle has 30 chars limit for all names,
43      *    2 chars are reserved for prefix.
44      *
45      * @const maximum length of field names
46      */
47     const NAME_MAX_LENGTH = 28;
49     /**
50      * Creates one new xmldb_table
51      * @param string $name
52      */
53     public function __construct($name) {
54         parent::__construct($name);
55         $this->fields = array();
56         $this->keys = array();
57         $this->indexes = array();
58     }
60     /**
61      * Add one field to the table, allowing to specify the desired  order
62      * If it's not specified, then the field is added at the end
63      * @param xmldb_field $field
64      * @param xmldb_object $after
65      * @return xmldb_field
66      */
67     public function addField($field, $after=null) {
69         // Detect duplicates first
70         if ($this->getField($field->getName())) {
71             throw new coding_exception('Duplicate field '.$field->getName().' specified in table '.$this->getName());
72         }
74         // Calculate the previous and next fields
75         $prevfield = null;
76         $nextfield = null;
78         if (!$after) {
79             $allfields = $this->getFields();
80             if (!empty($allfields)) {
81                 end($allfields);
82                 $prevfield = $allfields[key($allfields)];
83             }
84         } else {
85             $prevfield = $this->getField($after);
86         }
87         if ($prevfield && $prevfield->getNext()) {
88             $nextfield = $this->getField($prevfield->getNext());
89         }
91         // Set current field previous and next attributes
92         if ($prevfield) {
93             $field->setPrevious($prevfield->getName());
94             $prevfield->setNext($field->getName());
95         }
96         if ($nextfield) {
97             $field->setNext($nextfield->getName());
98             $nextfield->setPrevious($field->getName());
99         }
100         // Some more attributes
101         $field->setLoaded(true);
102         $field->setChanged(true);
103         // Add the new field
104         $this->fields[] = $field;
105         // Reorder the field
106         $this->orderFields($this->fields);
107         // Recalculate the hash
108         $this->calculateHash(true);
109         // We have one new field, so the table has changed
110         $this->setChanged(true);
112         return $field;
113     }
115     /**
116      * Add one key to the table, allowing to specify the desired  order
117      * If it's not specified, then the key is added at the end
118      * @param xmldb_key $key
119      * @param xmldb_object $after
120      */
121     public function addKey($key, $after=null) {
123         // Detect duplicates first
124         if ($this->getKey($key->getName())) {
125             throw new coding_exception('Duplicate key '.$key->getName().' specified in table '.$this->getName());
126         }
128         // Calculate the previous and next keys
129         $prevkey = null;
130         $nextkey = null;
132         if (!$after) {
133             $allkeys = $this->getKeys();
134             if (!empty($allkeys)) {
135                 end($allkeys);
136                 $prevkey = $allkeys[key($allkeys)];
137             }
138         } else {
139             $prevkey = $this->getKey($after);
140         }
141         if ($prevkey && $prevkey->getNext()) {
142             $nextkey = $this->getKey($prevkey->getNext());
143         }
145         // Set current key previous and next attributes
146         if ($prevkey) {
147             $key->setPrevious($prevkey->getName());
148             $prevkey->setNext($key->getName());
149         }
150         if ($nextkey) {
151             $key->setNext($nextkey->getName());
152             $nextkey->setPrevious($key->getName());
153         }
154         // Some more attributes
155         $key->setLoaded(true);
156         $key->setChanged(true);
157         // Add the new key
158         $this->keys[] = $key;
159         // Reorder the keys
160         $this->orderKeys($this->keys);
161         // Recalculate the hash
162         $this->calculateHash(true);
163         // We have one new field, so the table has changed
164         $this->setChanged(true);
165     }
167     /**
168      * Add one index to the table, allowing to specify the desired  order
169      * If it's not specified, then the index is added at the end
170      * @param xmldb_index $index
171      * @param xmldb_object $after
172      */
173     public function addIndex($index, $after=null) {
175         // Detect duplicates first
176         if ($this->getIndex($index->getName())) {
177             throw new coding_exception('Duplicate index '.$index->getName().' specified in table '.$this->getName());
178         }
180         // Calculate the previous and next indexes
181         $previndex = null;
182         $nextindex = null;
184         if (!$after) {
185             $allindexes = $this->getIndexes();
186             if (!empty($allindexes)) {
187                 end($allindexes);
188                 $previndex = $allindexes[key($allindexes)];
189             }
190         } else {
191             $previndex = $this->getIndex($after);
192         }
193         if ($previndex && $previndex->getNext()) {
194             $nextindex = $this->getIndex($previndex->getNext());
195         }
197         // Set current index previous and next attributes
198         if ($previndex) {
199             $index->setPrevious($previndex->getName());
200             $previndex->setNext($index->getName());
201         }
202         if ($nextindex) {
203             $index->setNext($nextindex->getName());
204             $nextindex->setPrevious($index->getName());
205         }
207         // Some more attributes
208         $index->setLoaded(true);
209         $index->setChanged(true);
210         // Add the new index
211         $this->indexes[] = $index;
212         // Reorder the indexes
213         $this->orderIndexes($this->indexes);
214         // Recalculate the hash
215         $this->calculateHash(true);
216         // We have one new index, so the table has changed
217         $this->setChanged(true);
218     }
220     /**
221      * This function will return the array of fields in the table
222      * @return array
223      */
224     public function getFields() {
225         return $this->fields;
226     }
228     /**
229      * This function will return the array of keys in the table
230      * @return array
231      */
232     public function getKeys() {
233         return $this->keys;
234     }
236     /**
237      * This function will return the array of indexes in the table
238      * @return array
239      */
240     public function getIndexes() {
241         return $this->indexes;
242     }
244     /**
245      * Returns one xmldb_field
246      * @param string $fieldname
247      * @return mixed
248      */
249     public function getField($fieldname) {
250         $i = $this->findFieldInArray($fieldname);
251         if ($i !== null) {
252             return $this->fields[$i];
253         }
254         return null;
255     }
257     /**
258      * Returns the position of one field in the array.
259      * @param string $fieldname
260      * @return mixed
261      */
262     public function findFieldInArray($fieldname) {
263         foreach ($this->fields as $i => $field) {
264             if ($fieldname == $field->getName()) {
265                 return $i;
266             }
267         }
268         return null;
269     }
271     /**
272      * This function will reorder the array of fields
273      * @return bool
274      */
275     public function orderFields() {
276         $result = $this->orderElements($this->fields);
277         if ($result) {
278             $this->setFields($result);
279             return true;
280         } else {
281             return false;
282         }
283     }
285     /**
286      * Returns one xmldb_key
287      * @param string $keyname
288      * @return mixed
289      */
290     public function getKey($keyname) {
291         $i = $this->findKeyInArray($keyname);
292         if ($i !== null) {
293             return $this->keys[$i];
294         }
295         return null;
296     }
298     /**
299      * Returns the position of one key in the array.
300      * @param string $keyname
301      * @return mixed
302      */
303     public function findKeyInArray($keyname) {
304         foreach ($this->keys as $i => $key) {
305             if ($keyname == $key->getName()) {
306                 return $i;
307             }
308         }
309         return null;
310     }
312     /**
313      * This function will reorder the array of keys
314      * @return bool
315      */
316     public function orderKeys() {
317         $result = $this->orderElements($this->keys);
318         if ($result) {
319             $this->setKeys($result);
320             return true;
321         } else {
322             return false;
323         }
324     }
326     /**
327      * Returns one xmldb_index
328      * @param string $indexname
329      * @return mixed
330      */
331     public function getIndex($indexname) {
332         $i = $this->findIndexInArray($indexname);
333         if ($i !== null) {
334             return $this->indexes[$i];
335         }
336         return null;
337     }
339     /**
340      * Returns the position of one index in the array.
341      * @param string $indexname
342      * @return mixed
343      */
344     public function findIndexInArray($indexname) {
345         foreach ($this->indexes as $i => $index) {
346             if ($indexname == $index->getName()) {
347                 return $i;
348             }
349         }
350         return null;
351     }
353     /**
354      * This function will reorder the array of indexes
355      * @return bool
356      */
357     public function orderIndexes() {
358         $result = $this->orderElements($this->indexes);
359         if ($result) {
360             $this->setIndexes($result);
361             return true;
362         } else {
363             return false;
364         }
365     }
367     /**
368      * This function will set the array of fields in the table
369      * @param array $fields
370      */
371     public function setFields($fields) {
372         $this->fields = $fields;
373     }
375     /**
376      * This function will set the array of keys in the table
377      * @param array $keys
378      */
379     public function setKeys($keys) {
380         $this->keys = $keys;
381     }
383     /**
384      * This function will set the array of indexes in the table
385      * @param array $indexes
386      */
387     public function setIndexes($indexes) {
388         $this->indexes = $indexes;
389     }
391     /**
392      * Delete one field from the table
393      * @param string $fieldname
394      */
395     public function deleteField($fieldname) {
397         $field = $this->getField($fieldname);
398         if ($field) {
399             $i = $this->findFieldInArray($fieldname);
400             // Look for prev and next field
401             $prevfield = $this->getField($field->getPrevious());
402             $nextfield = $this->getField($field->getNext());
403             // Change their previous and next attributes
404             if ($prevfield) {
405                 $prevfield->setNext($field->getNext());
406             }
407             if ($nextfield) {
408                 $nextfield->setPrevious($field->getPrevious());
409             }
410             // Delete the field
411             unset($this->fields[$i]);
412             // Reorder the whole structure
413             $this->orderFields($this->fields);
414             // Recalculate the hash
415             $this->calculateHash(true);
416             // We have one deleted field, so the table has changed
417             $this->setChanged(true);
418         }
419     }
421     /**
422      * Delete one key from the table
423      * @param string $keyname
424      */
425     public function deleteKey($keyname) {
427         $key = $this->getKey($keyname);
428         if ($key) {
429             $i = $this->findKeyInArray($keyname);
430             // Look for prev and next key
431             $prevkey = $this->getKey($key->getPrevious());
432             $nextkey = $this->getKey($key->getNext());
433             // Change their previous and next attributes
434             if ($prevkey) {
435                 $prevkey->setNext($key->getNext());
436             }
437             if ($nextkey) {
438                 $nextkey->setPrevious($key->getPrevious());
439             }
440             // Delete the key
441             unset($this->keys[$i]);
442             // Reorder the Keys
443             $this->orderKeys($this->keys);
444             // Recalculate the hash
445             $this->calculateHash(true);
446             // We have one deleted key, so the table has changed
447             $this->setChanged(true);
448         }
449     }
451     /**
452      * Delete one index from the table
453      * @param string $indexname
454      */
455     public function deleteIndex($indexname) {
457         $index = $this->getIndex($indexname);
458         if ($index) {
459             $i = $this->findIndexInArray($indexname);
460             // Look for prev and next index
461             $previndex = $this->getIndex($index->getPrevious());
462             $nextindex = $this->getIndex($index->getNext());
463             // Change their previous and next attributes
464             if ($previndex) {
465                 $previndex->setNext($index->getNext());
466             }
467             if ($nextindex) {
468                 $nextindex->setPrevious($index->getPrevious());
469             }
470             // Delete the index
471             unset($this->indexes[$i]);
472             // Reorder the indexes
473             $this->orderIndexes($this->indexes);
474             // Recalculate the hash
475             $this->calculateHash(true);
476             // We have one deleted index, so the table has changed
477             $this->setChanged(true);
478         }
479     }
481     /**
482      * Load data from XML to the table
483      * @param array $xmlarr
484      * @return bool success
485      */
486     public function arr2xmldb_table($xmlarr) {
488         global $CFG;
490         $result = true;
492         // Debug the table
493         // traverse_xmlize($xmlarr);                   //Debug
494         // print_object ($GLOBALS['traverse_array']);  //Debug
495         // $GLOBALS['traverse_array']="";              //Debug
497         // Process table attributes (name, comment, previoustable and nexttable)
498         if (isset($xmlarr['@']['NAME'])) {
499             $this->name = trim($xmlarr['@']['NAME']);
500         } else {
501             $this->errormsg = 'Missing NAME attribute';
502             $this->debug($this->errormsg);
503             $result = false;
504         }
505         if (isset($xmlarr['@']['COMMENT'])) {
506             $this->comment = trim($xmlarr['@']['COMMENT']);
507         } else if (!empty($CFG->xmldbdisablecommentchecking)) {
508             $this->comment = '';
509         } else {
510             $this->errormsg = 'Missing COMMENT attribute';
511             $this->debug($this->errormsg);
512             $result = false;
513         }
515         // Iterate over fields
516         if (isset($xmlarr['#']['FIELDS']['0']['#']['FIELD'])) {
517             foreach ($xmlarr['#']['FIELDS']['0']['#']['FIELD'] as $xmlfield) {
518                 if (!$result) { //Skip on error
519                     continue;
520                 }
521                 $name = trim($xmlfield['@']['NAME']);
522                 $field = new xmldb_field($name);
523                 $field->arr2xmldb_field($xmlfield);
524                 $this->fields[] = $field;
525                 if (!$field->isLoaded()) {
526                     $this->errormsg = 'Problem loading field ' . $name;
527                     $this->debug($this->errormsg);
528                     $result = false;
529                 }
530             }
531         } else {
532             $this->errormsg = 'Missing FIELDS section';
533             $this->debug($this->errormsg);
534             $result = false;
535         }
537         // Perform some general checks over fields
538         if ($result && $this->fields) {
539             // Check field names are ok (lowercase, a-z _-)
540             if (!$this->checkNameValues($this->fields)) {
541                 $this->errormsg = 'Some FIELDS name values are incorrect';
542                 $this->debug($this->errormsg);
543                 $result = false;
544             }
545             // Compute prev/next.
546             $this->fixPrevNext($this->fields);
547             // Order fields
548             if ($result && !$this->orderFields($this->fields)) {
549                 $this->errormsg = 'Error ordering the fields';
550                 $this->debug($this->errormsg);
551                 $result = false;
552             }
553         }
555         // Iterate over keys
556         if (isset($xmlarr['#']['KEYS']['0']['#']['KEY'])) {
557             foreach ($xmlarr['#']['KEYS']['0']['#']['KEY'] as $xmlkey) {
558                 if (!$result) { //Skip on error
559                     continue;
560                 }
561                 $name = trim($xmlkey['@']['NAME']);
562                 $key = new xmldb_key($name);
563                 $key->arr2xmldb_key($xmlkey);
564                 $this->keys[] = $key;
565                 if (!$key->isLoaded()) {
566                     $this->errormsg = 'Problem loading key ' . $name;
567                     $this->debug($this->errormsg);
568                     $result = false;
569                 }
570             }
571         } else {
572             $this->errormsg = 'Missing KEYS section (at least one PK must exist)';
573             $this->debug($this->errormsg);
574             $result = false;
575         }
577         // Perform some general checks over keys
578         if ($result && $this->keys) {
579             // Check keys names are ok (lowercase, a-z _-)
580             if (!$this->checkNameValues($this->keys)) {
581                 $this->errormsg = 'Some KEYS name values are incorrect';
582                 $this->debug($this->errormsg);
583                 $result = false;
584             }
585             // Compute prev/next.
586             $this->fixPrevNext($this->keys);
587             // Order keys
588             if ($result && !$this->orderKeys($this->keys)) {
589                 $this->errormsg = 'Error ordering the keys';
590                 $this->debug($this->errormsg);
591                 $result = false;
592             }
593             // TODO: Only one PK
594             // TODO: Not keys with repeated fields
595             // TODO: Check fields and reffieds exist in table
596         }
598         // Iterate over indexes
599         if (isset($xmlarr['#']['INDEXES']['0']['#']['INDEX'])) {
600             foreach ($xmlarr['#']['INDEXES']['0']['#']['INDEX'] as $xmlindex) {
601                 if (!$result) { //Skip on error
602                     continue;
603                 }
604                 $name = trim($xmlindex['@']['NAME']);
605                 $index = new xmldb_index($name);
606                 $index->arr2xmldb_index($xmlindex);
607                 $this->indexes[] = $index;
608                 if (!$index->isLoaded()) {
609                     $this->errormsg = 'Problem loading index ' . $name;
610                     $this->debug($this->errormsg);
611                     $result = false;
612                 }
613             }
614         }
616         // Perform some general checks over indexes
617         if ($result && $this->indexes) {
618             // Check field names are ok (lowercase, a-z _-)
619             if (!$this->checkNameValues($this->indexes)) {
620                 $this->errormsg = 'Some INDEXES name values are incorrect';
621                 $this->debug($this->errormsg);
622                 $result = false;
623             }
624             // Compute prev/next.
625             $this->fixPrevNext($this->indexes);
626             // Order indexes
627             if ($result && !$this->orderIndexes($this->indexes)) {
628                 $this->errormsg = 'Error ordering the indexes';
629                 $this->debug($this->errormsg);
630                 $result = false;
631             }
632             // TODO: Not indexes with repeated fields
633             // TODO: Check fields exist in table
634         }
636         // Set some attributes
637         if ($result) {
638             $this->loaded = true;
639         }
640         $this->calculateHash();
641         return $result;
642     }
644     /**
645      * This function calculate and set the hash of one xmldb_table
646      * @param bool $recursive
647      */
648      public function calculateHash($recursive = false) {
649         if (!$this->loaded) {
650             $this->hash = null;
651         } else {
652             $key = $this->name . $this->comment;
653             if ($this->fields) {
654                 foreach ($this->fields as $fie) {
655                     $field = $this->getField($fie->getName());
656                     if ($recursive) {
657                         $field->calculateHash($recursive);
658                     }
659                     $key .= $field->getHash();
660                 }
661             }
662             if ($this->keys) {
663                 foreach ($this->keys as $ke) {
664                     $k = $this->getKey($ke->getName());
665                     if ($recursive) {
666                         $k->calculateHash($recursive);
667                     }
668                     $key .= $k->getHash();
669                 }
670             }
671             if ($this->indexes) {
672                 foreach ($this->indexes as $in) {
673                     $index = $this->getIndex($in->getName());
674                     if ($recursive) {
675                         $index->calculateHash($recursive);
676                     }
677                     $key .= $index->getHash();
678                 }
679             }
680             $this->hash = md5($key);
681         }
682     }
684     /**
685      * Validates the table restrictions (does not validate child elements).
686      *
687      * The error message should not be localised because it is intended for developers,
688      * end users and admins should never see these problems!
689      *
690      * @param xmldb_table $xmldb_table optional when object is table
691      * @return string null if ok, error message if problem found
692      */
693     public function validateDefinition(xmldb_table $xmldb_table=null) {
694         // table parameter is ignored
695         $name = $this->getName();
696         if (strlen($name) > self::NAME_MAX_LENGTH) {
697             return 'Invalid table name {'.$name.'}: name is too long. Limit is 28 chars.';
698         }
699         if (!preg_match('/^[a-z][a-z0-9_]*$/', $name)) {
700             return 'Invalid table name {'.$name.'}: name includes invalid characters.';
701         }
703         return null;
704     }
706     /**
707      * This function will output the XML text for one table
708      * @return string
709      */
710     public function xmlOutput() {
711         $o = '';
712         $o.= '    <TABLE NAME="' . $this->name . '"';
713         if ($this->comment) {
714             $o.= ' COMMENT="' . htmlspecialchars($this->comment) . '"';
715         }
716         $o.= '>' . "\n";
717         // Now the fields
718         if ($this->fields) {
719             $o.= '      <FIELDS>' . "\n";
720             foreach ($this->fields as $field) {
721                 $o.= $field->xmlOutput();
722             }
723             $o.= '      </FIELDS>' . "\n";
724         }
725         // Now the keys
726         if ($this->keys) {
727             $o.= '      <KEYS>' . "\n";
728             foreach ($this->keys as $key) {
729                 $o.= $key->xmlOutput();
730             }
731             $o.= '      </KEYS>' . "\n";
732         }
733         // Now the indexes
734         if ($this->indexes) {
735             $o.= '      <INDEXES>' . "\n";
736             foreach ($this->indexes as $index) {
737                 $o.= $index->xmlOutput();
738             }
739             $o.= '      </INDEXES>' . "\n";
740         }
741         $o.= '    </TABLE>' . "\n";
743         return $o;
744     }
746     /**
747      * This function will add one new field to the table with all
748      * its attributes defined
749      *
750      * @param string $name name of the field
751      * @param int $type XMLDB_TYPE_INTEGER, XMLDB_TYPE_NUMBER, XMLDB_TYPE_CHAR, XMLDB_TYPE_TEXT, XMLDB_TYPE_BINARY
752      * @param string $precision length for integers and chars, two-comma separated numbers for numbers
753      * @param bool $unsigned XMLDB_UNSIGNED or null (or false)
754      * @param bool $notnull XMLDB_NOTNULL or null (or false)
755      * @param bool $sequence XMLDB_SEQUENCE or null (or false)
756      * @param mixed $default meaningful default o null (or false)
757      * @param xmldb_object $previous name of the previous field in the table or null (or false)
758      * @return xmlddb_field
759      */
760     public function add_field($name, $type, $precision=null, $unsigned=null, $notnull=null, $sequence=null, $default=null, $previous=null) {
761         $field = new xmldb_field($name, $type, $precision, $unsigned, $notnull, $sequence, $default);
762         $this->addField($field, $previous);
764         return $field;
765     }
767     /**
768      * This function will add one new key to the table with all
769      * its attributes defined
770      *
771      * @param string $name name of the key
772      * @param int $type XMLDB_KEY_PRIMARY, XMLDB_KEY_UNIQUE, XMLDB_KEY_FOREIGN
773      * @param array $fields an array of fieldnames to build the key over
774      * @param string $reftable name of the table the FK points to or null
775      * @param array $reffields an array of fieldnames in the FK table or null
776      */
777     public function add_key($name, $type, $fields, $reftable=null, $reffields=null) {
778         $key = new xmldb_key($name, $type, $fields, $reftable, $reffields);
779         $this->addKey($key);
780     }
782     /**
783      * This function will add one new index to the table with all
784      * its attributes defined
785      *
786      * @param string $name name of the index
787      * @param int $type XMLDB_INDEX_UNIQUE, XMLDB_INDEX_NOTUNIQUE
788      * @param array $fields an array of fieldnames to build the index over
789      * @param array $hints optional index type hints
790      */
791     public function add_index($name, $type, $fields, $hints = array()) {
792         $index = new xmldb_index($name, $type, $fields, $hints);
793         $this->addIndex($index);
794     }
796     /**
797      * This function will return all the errors found in one table
798      * looking recursively inside each field/key/index. Returns
799      * an array of errors or false
800      */
801     public function getAllErrors() {
803         $errors = array();
804         // First the table itself
805         if ($this->getError()) {
806             $errors[] = $this->getError();
807         }
808         // Delegate to fields
809         if ($fields = $this->getFields()) {
810             foreach ($fields as $field) {
811                 if ($field->getError()) {
812                     $errors[] = $field->getError();
813                 }
814             }
815         }
816         // Delegate to keys
817         if ($keys = $this->getKeys()) {
818             foreach ($keys as $key) {
819                 if ($key->getError()) {
820                     $errors[] = $key->getError();
821                 }
822             }
823         }
824         // Delegate to indexes
825         if ($indexes = $this->getIndexes()) {
826             foreach ($indexes as $index) {
827                 if ($index->getError()) {
828                     $errors[] = $index->getError();
829                 }
830             }
831         }
832         // Return decision
833         if (count($errors)) {
834             return $errors;
835         } else {
836             return false;
837         }
838     }