Merge branch 'MDL-81073' of https://github.com/paulholden/moodle
[moodle.git] / lib / xmldb / xmldb_table.php
blob5c78f14034bb51b5d08257fd1d0b78090c9b851c
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * This class represent one XMLDB table
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
26 defined('MOODLE_INTERNAL') || die();
29 class xmldb_table extends xmldb_object {
31 /** @var xmldb_field[] table columns */
32 protected $fields;
34 /** @var xmldb_key[] keys */
35 protected $keys;
37 /** @var xmldb_index[] indexes */
38 protected $indexes;
40 /** @var int max length of table name prefixes */
41 const PREFIX_MAX_LENGTH = 10;
43 /**
44 * Note:
45 * - PostgreSQL has a limit of 63 ascii chars (bytes) for table names. Others have greater limits.
46 * Up to PREFIX_MAX_LENGTH ascii chars (bytes) are reserved for table prefixes.
48 * @var int max length of table names (without prefix).
50 const NAME_MAX_LENGTH = 63 - self::PREFIX_MAX_LENGTH;
52 /**
53 * Creates one new xmldb_table
54 * @param string $name
56 public function __construct($name) {
57 parent::__construct($name);
58 $this->fields = array();
59 $this->keys = array();
60 $this->indexes = array();
63 /**
64 * Add one field to the table, allowing to specify the desired order
65 * If it's not specified, then the field is added at the end
66 * @param xmldb_field $field
67 * @param xmldb_object $after
68 * @return xmldb_field
70 public function addField($field, $after=null) {
72 // Detect duplicates first
73 if ($this->getField($field->getName())) {
74 throw new coding_exception('Duplicate field '.$field->getName().' specified in table '.$this->getName());
77 // Calculate the previous and next fields
78 $prevfield = null;
79 $nextfield = null;
81 if (!$after) {
82 $allfields = $this->getFields();
83 if (!empty($allfields)) {
84 end($allfields);
85 $prevfield = $allfields[key($allfields)];
87 } else {
88 $prevfield = $this->getField($after);
90 if ($prevfield && $prevfield->getNext()) {
91 $nextfield = $this->getField($prevfield->getNext());
94 // Set current field previous and next attributes
95 if ($prevfield) {
96 $field->setPrevious($prevfield->getName());
97 $prevfield->setNext($field->getName());
99 if ($nextfield) {
100 $field->setNext($nextfield->getName());
101 $nextfield->setPrevious($field->getName());
103 // Some more attributes
104 $field->setLoaded(true);
105 $field->setChanged(true);
106 // Add the new field
107 $this->fields[] = $field;
108 // Reorder the field
109 $this->orderFields($this->fields);
110 // Recalculate the hash
111 $this->calculateHash(true);
112 // We have one new field, so the table has changed
113 $this->setChanged(true);
115 return $field;
119 * Add one key to the table, allowing to specify the desired order
120 * If it's not specified, then the key is added at the end
121 * @param xmldb_key $key
122 * @param xmldb_object $after
124 public function addKey($key, $after=null) {
126 // Detect duplicates first
127 if ($this->getKey($key->getName())) {
128 throw new coding_exception('Duplicate key '.$key->getName().' specified in table '.$this->getName());
131 // Make sure there are no indexes with the key column specs because they would collide.
132 $newfields = $key->getFields();
133 $allindexes = $this->getIndexes();
134 foreach ($allindexes as $index) {
135 $fields = $index->getFields();
136 if ($fields === $newfields) {
137 throw new coding_exception('Index '.$index->getName().' collides with key'.$key->getName().' specified in table '.$this->getName());
141 // Calculate the previous and next keys
142 $prevkey = null;
143 $nextkey = null;
145 if (!$after) {
146 $allkeys = $this->getKeys();
147 if (!empty($allkeys)) {
148 end($allkeys);
149 $prevkey = $allkeys[key($allkeys)];
151 } else {
152 $prevkey = $this->getKey($after);
154 if ($prevkey && $prevkey->getNext()) {
155 $nextkey = $this->getKey($prevkey->getNext());
158 // Set current key previous and next attributes
159 if ($prevkey) {
160 $key->setPrevious($prevkey->getName());
161 $prevkey->setNext($key->getName());
163 if ($nextkey) {
164 $key->setNext($nextkey->getName());
165 $nextkey->setPrevious($key->getName());
167 // Some more attributes
168 $key->setLoaded(true);
169 $key->setChanged(true);
170 // Add the new key
171 $this->keys[] = $key;
172 // Reorder the keys
173 $this->orderKeys($this->keys);
174 // Recalculate the hash
175 $this->calculateHash(true);
176 // We have one new field, so the table has changed
177 $this->setChanged(true);
181 * Add one index to the table, allowing to specify the desired order
182 * If it's not specified, then the index is added at the end
183 * @param xmldb_index $index
184 * @param xmldb_object $after
186 public function addIndex($index, $after=null) {
188 // Detect duplicates first
189 if ($this->getIndex($index->getName())) {
190 throw new coding_exception('Duplicate index '.$index->getName().' specified in table '.$this->getName());
193 // Make sure there are no keys with the index column specs because they would collide.
194 $newfields = $index->getFields();
195 $allkeys = $this->getKeys();
196 foreach ($allkeys as $key) {
197 $fields = $key->getFields();
198 if ($fields === $newfields) {
199 throw new coding_exception('Key '.$key->getName().' collides with index'.$index->getName().' specified in table '.$this->getName());
203 // Calculate the previous and next indexes
204 $previndex = null;
205 $nextindex = null;
207 if (!$after) {
208 $allindexes = $this->getIndexes();
209 if (!empty($allindexes)) {
210 end($allindexes);
211 $previndex = $allindexes[key($allindexes)];
213 } else {
214 $previndex = $this->getIndex($after);
216 if ($previndex && $previndex->getNext()) {
217 $nextindex = $this->getIndex($previndex->getNext());
220 // Set current index previous and next attributes
221 if ($previndex) {
222 $index->setPrevious($previndex->getName());
223 $previndex->setNext($index->getName());
225 if ($nextindex) {
226 $index->setNext($nextindex->getName());
227 $nextindex->setPrevious($index->getName());
230 // Some more attributes
231 $index->setLoaded(true);
232 $index->setChanged(true);
233 // Add the new index
234 $this->indexes[] = $index;
235 // Reorder the indexes
236 $this->orderIndexes($this->indexes);
237 // Recalculate the hash
238 $this->calculateHash(true);
239 // We have one new index, so the table has changed
240 $this->setChanged(true);
244 * This function will return the array of fields in the table
245 * @return xmldb_field[]
247 public function getFields() {
248 return $this->fields;
252 * This function will return the array of keys in the table
253 * @return xmldb_key[]
255 public function getKeys() {
256 return $this->keys;
260 * This function will return the array of indexes in the table
261 * @return xmldb_index[]
263 public function getIndexes() {
264 return $this->indexes;
268 * Returns one xmldb_field
269 * @param string $fieldname
270 * @return xmldb_field|null
272 public function getField($fieldname) {
273 $i = $this->findFieldInArray($fieldname);
274 if ($i !== null) {
275 return $this->fields[$i];
277 return null;
281 * Returns the position of one field in the array.
282 * @param string $fieldname
283 * @return int|null index of the field, or null if not found.
285 public function findFieldInArray($fieldname) {
286 foreach ($this->fields as $i => $field) {
287 if ($fieldname == $field->getName()) {
288 return $i;
291 return null;
295 * This function will reorder the array of fields
296 * @return bool whether the reordering succeeded.
298 public function orderFields() {
299 $result = $this->orderElements($this->fields);
300 if ($result) {
301 $this->setFields($result);
302 return true;
303 } else {
304 return false;
309 * Returns one xmldb_key
310 * @param string $keyname
311 * @return xmldb_key|null
313 public function getKey($keyname) {
314 $i = $this->findKeyInArray($keyname);
315 if ($i !== null) {
316 return $this->keys[$i];
318 return null;
322 * Returns the position of one key in the array.
323 * @param string $keyname
324 * @return int|null index of the key, or null if not found.
326 public function findKeyInArray($keyname) {
327 foreach ($this->keys as $i => $key) {
328 if ($keyname == $key->getName()) {
329 return $i;
332 return null;
336 * This function will reorder the array of keys
337 * @return bool whether the reordering succeeded.
339 public function orderKeys() {
340 $result = $this->orderElements($this->keys);
341 if ($result) {
342 $this->setKeys($result);
343 return true;
344 } else {
345 return false;
350 * Returns one xmldb_index
351 * @param string $indexname
352 * @return xmldb_index|null
354 public function getIndex($indexname) {
355 $i = $this->findIndexInArray($indexname);
356 if ($i !== null) {
357 return $this->indexes[$i];
359 return null;
363 * Returns the position of one index in the array.
364 * @param string $indexname
365 * @return int|null index of the index, or null if not found.
367 public function findIndexInArray($indexname) {
368 foreach ($this->indexes as $i => $index) {
369 if ($indexname == $index->getName()) {
370 return $i;
373 return null;
377 * This function will reorder the array of indexes
378 * @return bool whether the reordering succeeded.
380 public function orderIndexes() {
381 $result = $this->orderElements($this->indexes);
382 if ($result) {
383 $this->setIndexes($result);
384 return true;
385 } else {
386 return false;
391 * This function will set the array of fields in the table
392 * @param xmldb_field[] $fields
394 public function setFields($fields) {
395 $this->fields = $fields;
399 * This function will set the array of keys in the table
400 * @param xmldb_key[] $keys
402 public function setKeys($keys) {
403 $this->keys = $keys;
407 * This function will set the array of indexes in the table
408 * @param xmldb_index[] $indexes
410 public function setIndexes($indexes) {
411 $this->indexes = $indexes;
415 * Delete one field from the table
416 * @param string $fieldname
418 public function deleteField($fieldname) {
420 $field = $this->getField($fieldname);
421 if ($field) {
422 $i = $this->findFieldInArray($fieldname);
423 // Look for prev and next field
424 $prevfield = $this->getField($field->getPrevious());
425 $nextfield = $this->getField($field->getNext());
426 // Change their previous and next attributes
427 if ($prevfield) {
428 $prevfield->setNext($field->getNext());
430 if ($nextfield) {
431 $nextfield->setPrevious($field->getPrevious());
433 // Delete the field
434 unset($this->fields[$i]);
435 // Reorder the whole structure
436 $this->orderFields($this->fields);
437 // Recalculate the hash
438 $this->calculateHash(true);
439 // We have one deleted field, so the table has changed
440 $this->setChanged(true);
445 * Delete one key from the table
446 * @param string $keyname
448 public function deleteKey($keyname) {
450 $key = $this->getKey($keyname);
451 if ($key) {
452 $i = $this->findKeyInArray($keyname);
453 // Look for prev and next key
454 $prevkey = $this->getKey($key->getPrevious());
455 $nextkey = $this->getKey($key->getNext());
456 // Change their previous and next attributes
457 if ($prevkey) {
458 $prevkey->setNext($key->getNext());
460 if ($nextkey) {
461 $nextkey->setPrevious($key->getPrevious());
463 // Delete the key
464 unset($this->keys[$i]);
465 // Reorder the Keys
466 $this->orderKeys($this->keys);
467 // Recalculate the hash
468 $this->calculateHash(true);
469 // We have one deleted key, so the table has changed
470 $this->setChanged(true);
475 * Delete one index from the table
476 * @param string $indexname
478 public function deleteIndex($indexname) {
480 $index = $this->getIndex($indexname);
481 if ($index) {
482 $i = $this->findIndexInArray($indexname);
483 // Look for prev and next index
484 $previndex = $this->getIndex($index->getPrevious());
485 $nextindex = $this->getIndex($index->getNext());
486 // Change their previous and next attributes
487 if ($previndex) {
488 $previndex->setNext($index->getNext());
490 if ($nextindex) {
491 $nextindex->setPrevious($index->getPrevious());
493 // Delete the index
494 unset($this->indexes[$i]);
495 // Reorder the indexes
496 $this->orderIndexes($this->indexes);
497 // Recalculate the hash
498 $this->calculateHash(true);
499 // We have one deleted index, so the table has changed
500 $this->setChanged(true);
505 * Load data from XML to the table
506 * @param array $xmlarr
507 * @return bool success
509 public function arr2xmldb_table($xmlarr) {
511 global $CFG;
513 $result = true;
515 // Debug the table
516 // traverse_xmlize($xmlarr); //Debug
517 // print_object ($GLOBALS['traverse_array']); //Debug
518 // $GLOBALS['traverse_array']=""; //Debug
520 // Process table attributes (name, comment, previoustable and nexttable)
521 if (isset($xmlarr['@']['NAME'])) {
522 $this->name = trim($xmlarr['@']['NAME']);
523 } else {
524 $this->errormsg = 'Missing NAME attribute';
525 $this->debug($this->errormsg);
526 $result = false;
528 if (isset($xmlarr['@']['COMMENT'])) {
529 $this->comment = trim($xmlarr['@']['COMMENT']);
530 } else if (!empty($CFG->xmldbdisablecommentchecking)) {
531 $this->comment = '';
532 } else {
533 $this->errormsg = 'Missing COMMENT attribute';
534 $this->debug($this->errormsg);
535 $result = false;
538 // Iterate over fields
539 if (isset($xmlarr['#']['FIELDS']['0']['#']['FIELD'])) {
540 foreach ($xmlarr['#']['FIELDS']['0']['#']['FIELD'] as $xmlfield) {
541 if (!$result) { //Skip on error
542 continue;
544 $name = trim($xmlfield['@']['NAME']);
545 $field = new xmldb_field($name);
546 $field->arr2xmldb_field($xmlfield);
547 $this->fields[] = $field;
548 if (!$field->isLoaded()) {
549 $this->errormsg = 'Problem loading field ' . $name;
550 $this->debug($this->errormsg);
551 $result = false;
554 } else {
555 $this->errormsg = 'Missing FIELDS section';
556 $this->debug($this->errormsg);
557 $result = false;
560 // Perform some general checks over fields
561 if ($result && $this->fields) {
562 // Check field names are ok (lowercase, a-z _-)
563 if (!$this->checkNameValues($this->fields)) {
564 $this->errormsg = 'Some FIELDS name values are incorrect';
565 $this->debug($this->errormsg);
566 $result = false;
568 // Compute prev/next.
569 $this->fixPrevNext($this->fields);
570 // Order fields
571 if ($result && !$this->orderFields($this->fields)) {
572 $this->errormsg = 'Error ordering the fields';
573 $this->debug($this->errormsg);
574 $result = false;
578 // Iterate over keys
579 if (isset($xmlarr['#']['KEYS']['0']['#']['KEY'])) {
580 foreach ($xmlarr['#']['KEYS']['0']['#']['KEY'] as $xmlkey) {
581 if (!$result) { //Skip on error
582 continue;
584 $name = trim($xmlkey['@']['NAME']);
585 $key = new xmldb_key($name);
586 $key->arr2xmldb_key($xmlkey);
587 $this->keys[] = $key;
588 if (!$key->isLoaded()) {
589 $this->errormsg = 'Problem loading key ' . $name;
590 $this->debug($this->errormsg);
591 $result = false;
594 } else {
595 $this->errormsg = 'Missing KEYS section (at least one PK must exist)';
596 $this->debug($this->errormsg);
597 $result = false;
600 // Perform some general checks over keys
601 if ($result && $this->keys) {
602 // Check keys names are ok (lowercase, a-z _-)
603 if (!$this->checkNameValues($this->keys)) {
604 $this->errormsg = 'Some KEYS name values are incorrect';
605 $this->debug($this->errormsg);
606 $result = false;
608 // Compute prev/next.
609 $this->fixPrevNext($this->keys);
610 // Order keys
611 if ($result && !$this->orderKeys($this->keys)) {
612 $this->errormsg = 'Error ordering the keys';
613 $this->debug($this->errormsg);
614 $result = false;
616 // TODO: Only one PK
617 // TODO: Not keys with repeated fields
618 // TODO: Check fields and reffieds exist in table
621 // Iterate over indexes
622 if (isset($xmlarr['#']['INDEXES']['0']['#']['INDEX'])) {
623 foreach ($xmlarr['#']['INDEXES']['0']['#']['INDEX'] as $xmlindex) {
624 if (!$result) { //Skip on error
625 continue;
627 $name = trim($xmlindex['@']['NAME']);
628 $index = new xmldb_index($name);
629 $index->arr2xmldb_index($xmlindex);
630 $this->indexes[] = $index;
631 if (!$index->isLoaded()) {
632 $this->errormsg = 'Problem loading index ' . $name;
633 $this->debug($this->errormsg);
634 $result = false;
639 // Perform some general checks over indexes
640 if ($result && $this->indexes) {
641 // Check field names are ok (lowercase, a-z _-)
642 if (!$this->checkNameValues($this->indexes)) {
643 $this->errormsg = 'Some INDEXES name values are incorrect';
644 $this->debug($this->errormsg);
645 $result = false;
647 // Compute prev/next.
648 $this->fixPrevNext($this->indexes);
649 // Order indexes
650 if ($result && !$this->orderIndexes($this->indexes)) {
651 $this->errormsg = 'Error ordering the indexes';
652 $this->debug($this->errormsg);
653 $result = false;
655 // TODO: Not indexes with repeated fields
656 // TODO: Check fields exist in table
659 // Set some attributes
660 if ($result) {
661 $this->loaded = true;
663 $this->calculateHash();
664 return $result;
668 * This function calculate and set the hash of one xmldb_table
669 * @param bool $recursive
671 public function calculateHash($recursive = false) {
672 if (!$this->loaded) {
673 $this->hash = null;
674 } else {
675 $key = $this->name . $this->comment;
676 if ($this->fields) {
677 foreach ($this->fields as $fie) {
678 $field = $this->getField($fie->getName());
679 if ($recursive) {
680 $field->calculateHash($recursive);
682 $key .= $field->getHash();
685 if ($this->keys) {
686 foreach ($this->keys as $ke) {
687 $k = $this->getKey($ke->getName());
688 if ($recursive) {
689 $k->calculateHash($recursive);
691 $key .= $k->getHash();
694 if ($this->indexes) {
695 foreach ($this->indexes as $in) {
696 $index = $this->getIndex($in->getName());
697 if ($recursive) {
698 $index->calculateHash($recursive);
700 $key .= $index->getHash();
703 $this->hash = md5($key);
708 * Validates the table restrictions (does not validate child elements).
710 * The error message should not be localised because it is intended for developers,
711 * end users and admins should never see these problems!
713 * @param xmldb_table $xmldb_table optional when object is table
714 * @return string null if ok, error message if problem found
716 public function validateDefinition(xmldb_table $xmldb_table=null) {
717 // table parameter is ignored
718 $name = $this->getName();
719 if (strlen($name) > self::NAME_MAX_LENGTH) {
720 return 'Invalid table name {'.$name.'}: name is too long. Limit is '.self::NAME_MAX_LENGTH.' chars.';
722 if (!preg_match('/^[a-z][a-z0-9_]*$/', $name)) {
723 return 'Invalid table name {'.$name.'}: name includes invalid characters.';
726 return null;
730 * This function will output the XML text for one table
731 * @return string
733 public function xmlOutput() {
734 $o = '';
735 $o.= ' <TABLE NAME="' . $this->name . '"';
736 if ($this->comment) {
737 $o.= ' COMMENT="' . htmlspecialchars($this->comment, ENT_COMPAT) . '"';
739 $o.= '>' . "\n";
740 // Now the fields
741 if ($this->fields) {
742 $o.= ' <FIELDS>' . "\n";
743 foreach ($this->fields as $field) {
744 $o.= $field->xmlOutput();
746 $o.= ' </FIELDS>' . "\n";
748 // Now the keys
749 if ($this->keys) {
750 $o.= ' <KEYS>' . "\n";
751 foreach ($this->keys as $key) {
752 $o.= $key->xmlOutput();
754 $o.= ' </KEYS>' . "\n";
756 // Now the indexes
757 if ($this->indexes) {
758 $o.= ' <INDEXES>' . "\n";
759 foreach ($this->indexes as $index) {
760 $o.= $index->xmlOutput();
762 $o.= ' </INDEXES>' . "\n";
764 $o.= ' </TABLE>' . "\n";
766 return $o;
770 * This function will add one new field to the table with all
771 * its attributes defined
773 * @param string $name name of the field
774 * @param int $type XMLDB_TYPE_INTEGER, XMLDB_TYPE_NUMBER, XMLDB_TYPE_CHAR, XMLDB_TYPE_TEXT, XMLDB_TYPE_BINARY
775 * @param string $precision length for integers and chars, two-comma separated numbers for numbers
776 * @param bool $unsigned XMLDB_UNSIGNED or null (or false)
777 * @param bool $notnull XMLDB_NOTNULL or null (or false)
778 * @param bool $sequence XMLDB_SEQUENCE or null (or false)
779 * @param mixed $default meaningful default o null (or false)
780 * @param xmldb_object $previous name of the previous field in the table or null (or false)
781 * @return xmlddb_field
783 public function add_field($name, $type, $precision=null, $unsigned=null, $notnull=null, $sequence=null, $default=null, $previous=null) {
784 $field = new xmldb_field($name, $type, $precision, $unsigned, $notnull, $sequence, $default);
785 $this->addField($field, $previous);
787 return $field;
791 * This function will add one new key to the table with all
792 * its attributes defined
794 * @param string $name name of the key
795 * @param int $type XMLDB_KEY_PRIMARY, XMLDB_KEY_UNIQUE, XMLDB_KEY_FOREIGN
796 * @param array $fields an array of fieldnames to build the key over
797 * @param string $reftable name of the table the FK points to or null
798 * @param array $reffields an array of fieldnames in the FK table or null
800 public function add_key($name, $type, $fields, $reftable=null, $reffields=null) {
801 $key = new xmldb_key($name, $type, $fields, $reftable, $reffields);
802 $this->addKey($key);
806 * This function will add one new index to the table with all
807 * its attributes defined
809 * @param string $name name of the index
810 * @param int $type XMLDB_INDEX_UNIQUE, XMLDB_INDEX_NOTUNIQUE
811 * @param array $fields an array of fieldnames to build the index over
812 * @param array $hints optional index type hints
814 public function add_index($name, $type, $fields, $hints = array()) {
815 $index = new xmldb_index($name, $type, $fields, $hints);
816 $this->addIndex($index);
820 * This function will return all the errors found in one table
821 * looking recursively inside each field/key/index. Returns
822 * an array of errors or false
824 public function getAllErrors() {
826 $errors = array();
827 // First the table itself
828 if ($this->getError()) {
829 $errors[] = $this->getError();
831 // Delegate to fields
832 if ($fields = $this->getFields()) {
833 foreach ($fields as $field) {
834 if ($field->getError()) {
835 $errors[] = $field->getError();
839 // Delegate to keys
840 if ($keys = $this->getKeys()) {
841 foreach ($keys as $key) {
842 if ($key->getError()) {
843 $errors[] = $key->getError();
847 // Delegate to indexes
848 if ($indexes = $this->getIndexes()) {
849 foreach ($indexes as $index) {
850 if ($index->getError()) {
851 $errors[] = $index->getError();
855 // Return decision
856 if (count($errors)) {
857 return $errors;
858 } else {
859 return false;